decred.org/dcrdex@v1.0.3/client/asset/zec/zec_test.go (about) 1 // This code is available on the terms of the project LICENSE.md file, 2 // also available online at https://blueoakcouncil.org/license/1.0.0. 3 4 package zec 5 6 import ( 7 "bytes" 8 "context" 9 "crypto/sha256" 10 "encoding/hex" 11 "encoding/json" 12 "errors" 13 "fmt" 14 "math/rand" 15 "os" 16 "sync" 17 "testing" 18 "time" 19 20 "decred.org/dcrdex/client/asset" 21 "decred.org/dcrdex/client/asset/btc" 22 "decred.org/dcrdex/dex" 23 "decred.org/dcrdex/dex/encode" 24 dexbtc "decred.org/dcrdex/dex/networks/btc" 25 dexzec "decred.org/dcrdex/dex/networks/zec" 26 "github.com/btcsuite/btcd/btcec/v2" 27 "github.com/btcsuite/btcd/btcjson" 28 "github.com/btcsuite/btcd/btcutil" 29 "github.com/btcsuite/btcd/chaincfg" 30 "github.com/btcsuite/btcd/chaincfg/chainhash" 31 "github.com/btcsuite/btcd/txscript" 32 "github.com/btcsuite/btcd/wire" 33 "github.com/decred/dcrd/dcrjson/v4" 34 ) 35 36 const ( 37 maxFutureBlockTime = 2 * time.Hour // see MaxTimeOffsetSeconds in btcd/blockchain/validate.go 38 tAddr = "tmH2m5fi5yY3Qg2GpGwcCrnnoD4wp944RMJ" 39 tUnifiedAddr = "uregtest1w2khftfjd8w7vgw32g24nwqdt0f3zfrwe9a4uy6trsq3swlpgsfssf2uqsfluu3490jdgers4vwz8l9yg39c9x70phllu8cy57f8mdc6ym5e7xtra0f99wtxvnm0wg4uz7an0wvl5s7jt2y7fqla2j976ej0e4hsspq73m0zrw5lzly797fhku0q74xeshuwvwmrku5u8f7gyd2r0sx" 40 ) 41 42 var ( 43 tCtx context.Context 44 tLogger = dex.StdOutLogger("T", dex.LevelError) 45 tTxID = "308e9a3675fc3ea3862b7863eeead08c621dcc37ff59de597dd3cdab41450ad9" 46 tTxHash *chainhash.Hash 47 tP2PKH []byte 48 tErr = errors.New("test error") 49 tLotSize uint64 = 1e6 50 ) 51 52 type msgBlockWithHeight struct { 53 msgBlock *dexzec.Block 54 height int64 55 } 56 57 const testBlocksPerBlockTimeOffset = 4 58 59 func generateTestBlockTime(blockHeight int64) time.Time { 60 return time.Unix(1e6, 0).Add(time.Duration(blockHeight) * maxFutureBlockTime / testBlocksPerBlockTimeOffset) 61 } 62 63 func makeRawTx(pkScripts []dex.Bytes, inputs []*wire.TxIn) *dexzec.Tx { 64 tx := &wire.MsgTx{ 65 TxIn: inputs, 66 } 67 for _, pkScript := range pkScripts { 68 tx.TxOut = append(tx.TxOut, wire.NewTxOut(1, pkScript)) 69 } 70 return dexzec.NewTxFromMsgTx(tx, dexzec.MaxExpiryHeight) 71 } 72 73 func dummyInput() *wire.TxIn { 74 return wire.NewTxIn(wire.NewOutPoint(&chainhash.Hash{0x01}, 0), nil, nil) 75 } 76 77 func dummyTx() *dexzec.Tx { 78 return makeRawTx([]dex.Bytes{encode.RandomBytes(32)}, []*wire.TxIn{dummyInput()}) 79 } 80 81 func signRawJSONTx(raw json.RawMessage) (*dexzec.Tx, []byte) { 82 var txB dex.Bytes 83 if err := json.Unmarshal(raw, &txB); err != nil { 84 panic(fmt.Sprintf("error unmarshaling tx: %v", err)) 85 } 86 tx, err := dexzec.DeserializeTx(txB) 87 if err != nil { 88 panic(fmt.Sprintf("error deserializing tx: %v", err)) 89 } 90 for i := range tx.TxIn { 91 tx.TxIn[i].SignatureScript = encode.RandomBytes(dexbtc.RedeemP2PKHSigScriptSize) 92 } 93 txB, err = tx.Bytes() 94 if err != nil { 95 panic(fmt.Sprintf("error serializing tx: %v", err)) 96 } 97 return tx, txB 98 } 99 100 func scriptHashAddress(contract []byte, chainParams *chaincfg.Params) (btcutil.Address, error) { 101 return btcutil.NewAddressScriptHash(contract, chainParams) 102 } 103 104 func makeSwapContract(lockTimeOffset time.Duration) (secret []byte, secretHash [32]byte, pkScript, contract []byte, addr, contractAddr btcutil.Address, lockTime time.Time) { 105 secret = encode.RandomBytes(32) 106 secretHash = sha256.Sum256(secret) 107 108 addr, _ = dexzec.DecodeAddress(tAddr, dexzec.RegressionNetAddressParams, dexzec.RegressionNetParams) 109 110 lockTime = time.Now().Add(lockTimeOffset) 111 contract, err := dexbtc.MakeContract(addr, addr, secretHash[:], lockTime.Unix(), false, &chaincfg.MainNetParams) 112 if err != nil { 113 panic("error making swap contract:" + err.Error()) 114 } 115 contractAddr, _ = scriptHashAddress(contract, &chaincfg.RegressionNetParams) 116 pkScript, _ = txscript.PayToAddrScript(contractAddr) 117 return 118 } 119 120 type tRPCClient struct { 121 tipChanged chan asset.WalletNotification 122 responses map[string][]any 123 124 // If there is an "any" key in the getTransactionMap, that value will be 125 // returned for all requests. Otherwise the tx id is looked up. 126 getTransactionMap map[string]*btc.GetTransactionResult 127 getTransactionErr error 128 129 blockchainMtx sync.RWMutex 130 verboseBlocks map[string]*msgBlockWithHeight 131 mainchain map[int64]*chainhash.Hash 132 getBestBlockHashErr error 133 } 134 135 func newRPCClient() *tRPCClient { 136 genesisHash := chaincfg.MainNetParams.GenesisHash 137 return &tRPCClient{ 138 responses: make(map[string][]any), 139 tipChanged: make(chan asset.WalletNotification), 140 verboseBlocks: map[string]*msgBlockWithHeight{ 141 genesisHash.String(): {msgBlock: &dexzec.Block{}}, 142 }, 143 mainchain: map[int64]*chainhash.Hash{ 144 0: genesisHash, 145 }, 146 } 147 } 148 149 func (c *tRPCClient) queueResponse(method string, resp any) { 150 c.responses[method] = append(c.responses[method], resp) 151 } 152 153 func (c *tRPCClient) checkEmptiness(t *testing.T) { 154 t.Helper() 155 var stillQueued = map[string]int{} 156 for method, resps := range c.responses { 157 if len(resps) > 0 { 158 stillQueued[method] = len(resps) 159 fmt.Printf("Method %s still has %d responses queued \n", method, len(resps)) 160 } 161 } 162 if len(stillQueued) > 0 { 163 t.Fatalf("response queue not empty: %+v", stillQueued) 164 } 165 } 166 167 func (c *tRPCClient) getBlock(blockHash string) *msgBlockWithHeight { 168 c.blockchainMtx.Lock() 169 defer c.blockchainMtx.Unlock() 170 return c.verboseBlocks[blockHash] 171 } 172 173 func (c *tRPCClient) bestBlock() (*chainhash.Hash, int64) { 174 c.blockchainMtx.RLock() 175 defer c.blockchainMtx.RUnlock() 176 var bestHash *chainhash.Hash 177 var bestBlkHeight int64 178 for height, hash := range c.mainchain { 179 if height >= bestBlkHeight { 180 bestBlkHeight = height 181 bestHash = hash 182 } 183 } 184 return bestHash, bestBlkHeight 185 } 186 187 func (c *tRPCClient) getBestBlockHeight() int64 { 188 c.blockchainMtx.RLock() 189 defer c.blockchainMtx.RUnlock() 190 var bestBlkHeight int64 191 for height := range c.mainchain { 192 if height >= bestBlkHeight { 193 bestBlkHeight = height 194 } 195 } 196 return bestBlkHeight 197 } 198 199 func (c *tRPCClient) addRawTx(blockHeight int64, tx *dexzec.Tx) (*chainhash.Hash, *dexzec.Block) { 200 c.blockchainMtx.Lock() 201 defer c.blockchainMtx.Unlock() 202 blockHash, found := c.mainchain[blockHeight] 203 if !found { 204 prevBlock := &chainhash.Hash{} 205 if blockHeight > 0 { 206 var exists bool 207 prevBlock, exists = c.mainchain[blockHeight-1] 208 if !exists { 209 prevBlock = &chainhash.Hash{} 210 } 211 } 212 nonce, bits := rand.Uint32(), rand.Uint32() 213 header := wire.NewBlockHeader(0, prevBlock, &chainhash.Hash{} /* lie, maybe fix this */, bits, nonce) 214 header.Timestamp = generateTestBlockTime(blockHeight) 215 blk := &dexzec.Block{MsgBlock: *wire.NewMsgBlock(header)} // only now do we know the block hash 216 hash := blk.BlockHash() 217 blockHash = &hash 218 c.verboseBlocks[blockHash.String()] = &msgBlockWithHeight{ 219 msgBlock: blk, 220 height: blockHeight, 221 } 222 c.mainchain[blockHeight] = blockHash 223 } 224 block := c.verboseBlocks[blockHash.String()] 225 block.msgBlock.AddTransaction(tx.MsgTx) 226 return blockHash, block.msgBlock 227 } 228 229 func (c *tRPCClient) RawRequest(ctx context.Context, method string, args []json.RawMessage) (json.RawMessage, error) { 230 respond := func(resp any) (json.RawMessage, error) { 231 b, err := json.Marshal(resp) 232 if err != nil { 233 panic(fmt.Sprintf("error marshaling test response: %v", err)) 234 } 235 return b, nil 236 } 237 238 responses := c.responses[method] 239 if len(responses) > 0 { 240 resp := c.responses[method][0] 241 c.responses[method] = c.responses[method][1:] 242 if err, is := resp.(error); is { 243 return nil, err 244 } 245 if f, is := resp.(func([]json.RawMessage) (json.RawMessage, error)); is { 246 return f(args) 247 } 248 return respond(resp) 249 } 250 251 // Some methods are handled by the testing infrastructure 252 switch method { 253 case "getbestblockhash": 254 c.blockchainMtx.RLock() 255 if c.getBestBlockHashErr != nil { 256 c.blockchainMtx.RUnlock() 257 return nil, c.getBestBlockHashErr 258 } 259 c.blockchainMtx.RUnlock() 260 bestHash, _ := c.bestBlock() 261 return respond(bestHash.String()) 262 case "getblockhash": 263 var blockHeight int64 264 if err := json.Unmarshal(args[0], &blockHeight); err != nil { 265 panic(fmt.Sprintf("error unmarshaling block height: %v", err)) 266 } 267 c.blockchainMtx.RLock() 268 defer c.blockchainMtx.RUnlock() 269 for height, blockHash := range c.mainchain { 270 if height == blockHeight { 271 return respond(blockHash.String()) 272 } 273 } 274 return nil, fmt.Errorf("block not found") 275 case "getblock": 276 c.blockchainMtx.Lock() 277 defer c.blockchainMtx.Unlock() 278 var blockHashStr string 279 if err := json.Unmarshal(args[0], &blockHashStr); err != nil { 280 panic(fmt.Sprintf("error unmarshaling block hash: %v", err)) 281 } 282 blk, found := c.verboseBlocks[blockHashStr] 283 if !found { 284 return nil, fmt.Errorf("block not found") 285 } 286 var buf bytes.Buffer 287 err := blk.msgBlock.Serialize(&buf) 288 if err != nil { 289 return nil, fmt.Errorf("block serialization error: %v", err) 290 } 291 return respond(hex.EncodeToString(buf.Bytes())) 292 case "getblockheader": 293 var blkHash string 294 if err := json.Unmarshal(args[0], &blkHash); err != nil { 295 panic(fmt.Sprintf("error unmarshaling block hash: %v", err)) 296 } 297 block := c.getBlock(blkHash) 298 if block == nil { 299 return nil, fmt.Errorf("no block verbose found") 300 } 301 // block may get modified concurrently, lock mtx before reading fields. 302 c.blockchainMtx.RLock() 303 defer c.blockchainMtx.RUnlock() 304 return respond(&btc.BlockHeader{ 305 Hash: block.msgBlock.BlockHash().String(), 306 Height: block.height, 307 // Confirmations: block.Confirmations, 308 // Time: block.Time, 309 }) 310 case "gettransaction": 311 if c.getTransactionErr != nil { 312 return nil, c.getTransactionErr 313 } 314 315 c.blockchainMtx.Lock() 316 defer c.blockchainMtx.Unlock() 317 var txID string 318 if err := json.Unmarshal(args[0], &txID); err != nil { 319 panic(fmt.Sprintf("error unmarshaling block hash: %v", err)) 320 } 321 var txData *btc.GetTransactionResult 322 if c.getTransactionMap != nil { 323 if txData = c.getTransactionMap["any"]; txData == nil { 324 txData = c.getTransactionMap[txID] 325 } 326 } 327 if txData == nil { 328 return nil, btc.WalletTransactionNotFound 329 } 330 return respond(txData) 331 case "signrawtransaction": 332 _, txB := signRawJSONTx(args[0]) 333 return respond(&btc.SignTxResult{ 334 Hex: txB, 335 Complete: true, 336 }) 337 } 338 return nil, fmt.Errorf("no test response queued for %q", method) 339 } 340 341 func boolPtr(v bool) *bool { 342 return &v 343 } 344 345 func tNewWallet() (*zecWallet, *tRPCClient, func()) { 346 dataDir, err := os.MkdirTemp("", "") 347 if err != nil { 348 panic("couldn't create data dir:" + err.Error()) 349 } 350 351 cl := newRPCClient() 352 walletCfg := &asset.WalletConfig{ 353 Emit: asset.NewWalletEmitter(cl.tipChanged, BipID, tLogger), 354 PeersChange: func(num uint32, err error) { 355 fmt.Printf("peer count = %d, err = %v", num, err) 356 }, 357 DataDir: dataDir, 358 Settings: map[string]string{ 359 "rpcuser": "a", 360 "rpcpassword": "b", 361 }, 362 } 363 walletCtx, shutdown := context.WithCancel(tCtx) 364 365 wi, err := NewWallet(walletCfg, tLogger, dex.Simnet) 366 if err != nil { 367 panic(fmt.Sprintf("NewWallet error: %v", err)) 368 } 369 370 w := wi.(*zecWallet) 371 w.node = cl 372 bestHash, _ := cl.bestBlock() 373 w.currentTip = &btc.BlockVector{ 374 Height: cl.getBestBlockHeight(), 375 Hash: *bestHash, 376 } 377 378 var wg sync.WaitGroup 379 wg.Add(1) 380 go func() { 381 defer wg.Done() 382 w.watchBlocks(walletCtx) 383 }() 384 shutdownAndWait := func() { 385 shutdown() 386 os.RemoveAll(dataDir) 387 wg.Wait() 388 } 389 return w, cl, shutdownAndWait 390 } 391 392 func TestMain(m *testing.M) { 393 tLogger = dex.StdOutLogger("TEST", dex.LevelCritical) 394 var shutdown func() 395 tCtx, shutdown = context.WithCancel(context.Background()) 396 tTxHash, _ = chainhash.NewHashFromStr(tTxID) 397 tP2PKH, _ = hex.DecodeString("76a9148fc02268f208a61767504fe0b48d228641ba81e388ac") 398 // tP2SH, _ = hex.DecodeString("76a91412a9abf5c32392f38bd8a1f57d81b1aeecc5699588ac") 399 doIt := func() int { 400 // Not counted as coverage, must test Archiver constructor explicitly. 401 defer shutdown() 402 return m.Run() 403 } 404 os.Exit(doIt()) 405 } 406 407 func TestFundOrder(t *testing.T) { 408 w, cl, shutdown := tNewWallet() 409 defer shutdown() 410 defer cl.checkEmptiness(t) 411 412 acctBal := &zAccountBalance{} 413 queueAccountBals := func() { 414 cl.queueResponse(methodZGetBalanceForAccount, acctBal) // 0-conf 415 cl.queueResponse(methodZGetBalanceForAccount, acctBal) // minOrchardConfs 416 cl.queueResponse(methodZGetNotesCount, &zNotesCount{Orchard: 1}) 417 } 418 419 // With an empty list returned, there should be no error, but the value zero 420 // should be returned. 421 unspents := make([]*btc.ListUnspentResult, 0) 422 queueAccountBals() 423 cl.queueResponse("listlockunspent", []*btc.RPCOutpoint{}) 424 425 bal, err := w.Balance() 426 if err != nil { 427 t.Fatalf("error for zero utxos: %v", err) 428 } 429 if bal.Available != 0 { 430 t.Fatalf("expected available = 0, got %d", bal.Available) 431 } 432 if bal.Immature != 0 { 433 t.Fatalf("expected unconf = 0, got %d", bal.Immature) 434 } 435 436 cl.queueResponse(methodZGetBalanceForAccount, tErr) 437 _, err = w.Balance() 438 if !errorHasCode(err, errBalanceRetrieval) { 439 t.Fatalf("wrong error for rpc error: %v", err) 440 } 441 442 var littleLots uint64 = 12 443 littleOrder := tLotSize * littleLots 444 littleFunds := dexzec.RequiredOrderFunds(littleOrder, 1, dexbtc.RedeemP2PKHInputSize, littleLots) 445 littleUTXO := &btc.ListUnspentResult{ 446 TxID: tTxID, 447 Address: "tmH2m5fi5yY3Qg2GpGwcCrnnoD4wp944RMJ", 448 Amount: float64(littleFunds) / 1e8, 449 Confirmations: 1, 450 ScriptPubKey: tP2PKH, 451 Spendable: true, 452 Solvable: true, 453 SafePtr: boolPtr(true), 454 } 455 unspents = append(unspents, littleUTXO) 456 457 lockedVal := uint64(1e6) 458 acctBal = &zAccountBalance{ 459 Pools: zBalancePools{ 460 Transparent: valZat{ 461 ValueZat: littleFunds - lockedVal, 462 }, 463 }, 464 } 465 466 lockedOutpoints := []*btc.RPCOutpoint{ 467 { 468 TxID: tTxID, 469 Vout: 1, 470 }, 471 } 472 473 queueAccountBals() 474 cl.queueResponse("gettxout", &btcjson.GetTxOutResult{}) 475 cl.queueResponse("listlockunspent", lockedOutpoints) 476 477 tx := makeRawTx([]dex.Bytes{{0x01}, {0x02}}, []*wire.TxIn{dummyInput()}) 478 tx.TxOut[1].Value = int64(lockedVal) 479 txB, _ := tx.Bytes() 480 const blockHeight = 5 481 blockHash, _ := cl.addRawTx(blockHeight, tx) 482 483 cl.getTransactionMap = map[string]*btc.GetTransactionResult{ 484 "any": { 485 BlockHash: blockHash.String(), 486 BlockIndex: blockHeight, 487 Bytes: txB, 488 }, 489 } 490 491 bal, err = w.Balance() 492 if err != nil { 493 t.Fatalf("error for 1 utxo: %v", err) 494 } 495 if bal.Available != littleFunds-lockedVal { 496 t.Fatalf("expected available = %d for confirmed utxos, got %d", littleOrder-lockedVal, bal.Available) 497 } 498 if bal.Immature != 0 { 499 t.Fatalf("expected immature = 0, got %d", bal.Immature) 500 } 501 if bal.Locked != lockedVal { 502 t.Fatalf("expected locked = %d, got %d", lockedVal, bal.Locked) 503 } 504 505 var lottaLots uint64 = 100 506 lottaOrder := tLotSize * lottaLots 507 // Add funding for an extra input to accommodate the later combined tests. 508 lottaFunds := dexzec.RequiredOrderFunds(lottaOrder, 1, dexbtc.RedeemP2PKHInputSize, lottaLots) 509 lottaUTXO := &btc.ListUnspentResult{ 510 TxID: tTxID, 511 Address: "tmH2m5fi5yY3Qg2GpGwcCrnnoD4wp944RMJ", 512 Amount: float64(lottaFunds) / 1e8, 513 Confirmations: 1, 514 Vout: 1, 515 ScriptPubKey: tP2PKH, 516 Spendable: true, 517 Solvable: true, 518 SafePtr: boolPtr(true), 519 } 520 unspents = append(unspents, lottaUTXO) 521 // littleUTXO.Confirmations = 1 522 // node.listUnspent = unspents 523 // bals.Mine.Trusted += float64(lottaFunds) / 1e8 524 // node.getBalances = &bals 525 acctBal.Pools.Transparent.ValueZat = littleFunds + lottaFunds - lockedVal 526 queueAccountBals() 527 cl.queueResponse("gettxout", &btcjson.GetTxOutResult{}) 528 cl.queueResponse("listlockunspent", lockedOutpoints) 529 bal, err = w.Balance() 530 if err != nil { 531 t.Fatalf("error for 2 utxos: %v", err) 532 } 533 if bal.Available != littleFunds+lottaFunds-lockedVal { 534 t.Fatalf("expected available = %d for 2 outputs, got %d", littleFunds+lottaFunds-lockedVal, bal.Available) 535 } 536 if bal.Immature != 0 { 537 t.Fatalf("expected immature = 0 for 2 outputs, got %d", bal.Immature) 538 } 539 540 ord := &asset.Order{ 541 Version: version, 542 Value: 0, 543 MaxSwapCount: 1, 544 Options: make(map[string]string), 545 } 546 547 setOrderValue := func(v uint64) { 548 ord.Value = v 549 ord.MaxSwapCount = v / tLotSize 550 } 551 552 queueBalances := func() { 553 cl.queueResponse(methodZGetBalanceForAccount, acctBal) // 0-conf 554 cl.queueResponse(methodZGetBalanceForAccount, acctBal) // minOrchardConfs 555 cl.queueResponse(methodZGetNotesCount, &zNotesCount{Orchard: nActionsOrchardEstimate}) 556 } 557 558 // Zero value 559 _, _, _, err = w.FundOrder(ord) 560 if !errorHasCode(err, errNoFundsRequested) { 561 t.Fatalf("wrong error for zero value: %v", err) 562 } 563 564 // Nothing to spend 565 acctBal.Pools.Transparent.ValueZat = 0 566 queueBalances() 567 cl.queueResponse("listunspent", []*btc.ListUnspentResult{}) 568 setOrderValue(littleOrder) 569 _, _, _, err = w.FundOrder(ord) 570 if !errorHasCode(err, errFunding) { 571 t.Fatalf("wrong error for zero utxos: %v", err) 572 } 573 574 // RPC error 575 acctBal.Pools.Transparent.ValueZat = littleFunds + lottaFunds 576 queueBalances() 577 cl.queueResponse("listunspent", tErr) 578 _, _, _, err = w.FundOrder(ord) 579 if !errorHasCode(err, errFunding) { 580 t.Fatalf("wrong funding error for rpc error: %v", err) 581 } 582 583 // Negative response when locking outputs. 584 queueBalances() 585 cl.queueResponse("listunspent", unspents) 586 cl.queueResponse("lockunspent", false) 587 _, _, _, err = w.FundOrder(ord) 588 if !errorHasCode(err, errLockUnspent) { 589 t.Fatalf("wrong error for lockunspent result = false: %v", err) 590 } 591 // New coin manager with no locked coins. Could also use ReturnCoins 592 w.prepareCoinManager() 593 594 queueSuccess := func() { 595 queueBalances() 596 cl.queueResponse("listunspent", unspents) 597 cl.queueResponse("lockunspent", true) 598 } 599 600 // Fund a little bit, with zero-conf littleUTXO. 601 littleUTXO.Confirmations = 0 602 lottaUTXO.Confirmations = 1 603 queueSuccess() 604 spendables, _, _, err := w.FundOrder(ord) 605 if err != nil { 606 t.Fatalf("error funding small amount: %v", err) 607 } 608 if len(spendables) != 1 { 609 t.Fatalf("expected 1 spendable, got %d", len(spendables)) 610 } 611 v := spendables[0].Value() 612 if v != lottaFunds { // has to pick the larger output 613 t.Fatalf("expected spendable of value %d, got %d", lottaFunds, v) 614 } 615 w.prepareCoinManager() 616 617 // Now with confirmed littleUTXO. 618 littleUTXO.Confirmations = 1 619 queueSuccess() 620 spendables, _, _, err = w.FundOrder(ord) 621 if err != nil { 622 t.Fatalf("error funding small amount: %v", err) 623 } 624 if len(spendables) != 1 { 625 t.Fatalf("expected 1 spendable, got %d", len(spendables)) 626 } 627 v = spendables[0].Value() 628 if v != littleFunds { 629 t.Fatalf("expected spendable of value %d, got %d", littleFunds, v) 630 } 631 w.prepareCoinManager() 632 633 // // Adding a fee bump should now require the larger UTXO. 634 // ord.Options = map[string]string{swapFeeBumpKey: "1.5"} 635 // spendables, _, _, err = wallet.FundOrder(ord) 636 // if err != nil { 637 // t.Fatalf("error funding bumped fees: %v", err) 638 // } 639 // if len(spendables) != 1 { 640 // t.Fatalf("expected 1 spendable, got %d", len(spendables)) 641 // } 642 // v = spendables[0].Value() 643 // if v != lottaFunds { // picks the bigger output because it is confirmed 644 // t.Fatalf("expected bumped fee utxo of value %d, got %d", littleFunds, v) 645 // } 646 // ord.Options = nil 647 // littleUTXO.Confirmations = 0 648 // _ = wallet.ReturnCoins(spendables) 649 650 // Make lottaOrder unconfirmed like littleOrder, favoring little now. 651 littleUTXO.Confirmations = 0 652 lottaUTXO.Confirmations = 0 653 queueSuccess() 654 spendables, _, _, err = w.FundOrder(ord) 655 if err != nil { 656 t.Fatalf("error funding small amount: %v", err) 657 } 658 if len(spendables) != 1 { 659 t.Fatalf("expected 1 spendable, got %d", len(spendables)) 660 } 661 v = spendables[0].Value() 662 if v != littleFunds { // now picks the smaller output 663 t.Fatalf("expected spendable of value %d, got %d", littleFunds, v) 664 } 665 w.prepareCoinManager() 666 667 // Fund a lotta bit, covered by just the lottaBit UTXO. 668 setOrderValue(lottaOrder) 669 queueSuccess() 670 spendables, _, fees, err := w.FundOrder(ord) 671 if err != nil { 672 t.Fatalf("error funding large amount: %v", err) 673 } 674 if len(spendables) != 1 { 675 t.Fatalf("expected 1 spendable, got %d", len(spendables)) 676 } 677 if fees != 0 { 678 t.Fatalf("expected no fees, got %d", fees) 679 } 680 v = spendables[0].Value() 681 if v != lottaFunds { 682 t.Fatalf("expected spendable of value %d, got %d", lottaFunds, v) 683 } 684 w.prepareCoinManager() 685 686 // require both spendables 687 extraLottaOrder := littleOrder + lottaOrder 688 setOrderValue(extraLottaOrder) 689 queueSuccess() 690 spendables, _, fees, err = w.FundOrder(ord) 691 if err != nil { 692 t.Fatalf("error funding large amount: %v", err) 693 } 694 if len(spendables) != 2 { 695 t.Fatalf("expected 2 spendable, got %d", len(spendables)) 696 } 697 if fees != 0 { 698 t.Fatalf("expected no split tx fees, got %d", fees) 699 } 700 v = spendables[0].Value() 701 if v != lottaFunds { 702 t.Fatalf("expected spendable of value %d, got %d", lottaFunds, v) 703 } 704 w.prepareCoinManager() 705 706 // Not enough to cover transaction fees. 707 extraLottaLots := littleLots + lottaLots 708 tweak := float64(littleFunds+lottaFunds-dexzec.RequiredOrderFunds(extraLottaOrder, 2, 2*dexbtc.RedeemP2PKHInputSize, extraLottaLots)+1) / 1e8 709 lottaUTXO.Amount -= tweak 710 // cl.queueResponse("listunspent", unspents) 711 _, _, _, err = w.FundOrder(ord) 712 if err == nil { 713 t.Fatalf("no error when not enough to cover tx fees") 714 } 715 lottaUTXO.Amount += tweak 716 w.prepareCoinManager() 717 718 // Prepare for a split transaction. 719 w.walletCfg.Load().(*WalletConfig).UseSplitTx = true 720 queueSuccess() 721 // No error when no split performed cuz math. 722 coins, _, fees, err := w.FundOrder(ord) 723 if err != nil { 724 t.Fatalf("error for no-split split: %v", err) 725 } 726 if fees != 0 { 727 t.Fatalf("no-split split returned non-zero fees: %d", fees) 728 } 729 // Should be both coins. 730 if len(coins) != 2 { 731 t.Fatalf("no-split split didn't return both coins") 732 } 733 w.prepareCoinManager() 734 735 // No split because not standing order. 736 ord.Immediate = true 737 queueSuccess() 738 coins, _, fees, err = w.FundOrder(ord) 739 if err != nil { 740 t.Fatalf("error for no-split split: %v", err) 741 } 742 ord.Immediate = false 743 if len(coins) != 2 { 744 t.Fatalf("no-split split didn't return both coins") 745 } 746 if fees != 0 { 747 t.Fatalf("no-split split returned non-zero fees: %d", fees) 748 } 749 w.prepareCoinManager() 750 751 var rawSent bool 752 queueSplit := func() { 753 cl.queueResponse(methodZGetAddressForAccount, &zGetAddressForAccountResult{Address: tAddr}) // split output 754 cl.queueResponse(methodZListUnifiedReceivers, &unifiedReceivers{Transparent: tAddr}) 755 cl.queueResponse(methodZGetAddressForAccount, &zGetAddressForAccountResult{Address: tAddr}) // change 756 cl.queueResponse(methodZListUnifiedReceivers, &unifiedReceivers{Transparent: tAddr}) 757 cl.queueResponse("sendrawtransaction", func(args []json.RawMessage) (json.RawMessage, error) { 758 rawSent = true 759 tx, _ := signRawJSONTx(args[0]) 760 return json.Marshal(tx.TxHash().String()) 761 }) 762 } 763 764 // With a little more available, the split should be performed. 765 baggageFees := dexzec.TxFeesZIP317(2*dexbtc.RedeemP2PKHInputSize+1, 2*dexbtc.P2PKHOutputSize+1, 0, 0, 0, 0) 766 lottaUTXO.Amount += (float64(baggageFees) + 1) / 1e8 767 queueSuccess() 768 queueSplit() 769 coins, _, fees, err = w.FundOrder(ord) 770 if err != nil { 771 t.Fatalf("error for split tx: %v", err) 772 } 773 // Should be just one coin. 774 if len(coins) != 1 { 775 t.Fatalf("split failed - coin count != 1. got %d", len(coins)) 776 } 777 if !rawSent { 778 t.Fatalf("split failed - no tx sent") 779 } 780 if fees != baggageFees { 781 t.Fatalf("split returned unexpected fees. wanted %d, got %d", baggageFees, fees) 782 } 783 w.prepareCoinManager() 784 785 // The split should also be added if we set the option at order time. 786 w.walletCfg.Load().(*WalletConfig).UseSplitTx = true 787 ord.Options = map[string]string{"swapsplit": "true"} 788 queueSuccess() 789 queueSplit() 790 coins, _, fees, err = w.FundOrder(ord) 791 if err != nil { 792 t.Fatalf("error for forced split tx: %v", err) 793 } 794 // Should be just one coin still. 795 if len(coins) != 1 { 796 t.Fatalf("forced split failed - coin count != 1") 797 } 798 if fees != baggageFees { 799 t.Fatalf("split returned unexpected fees. wanted %d, got %d", baggageFees, fees) 800 } 801 w.prepareCoinManager() 802 803 // Go back to just enough, but add reserves and get an error. 804 lottaUTXO.Amount = float64(lottaFunds) / 1e8 805 w.reserves.Store(1) 806 cl.queueResponse("listunspent", unspents) 807 queueBalances() 808 _, _, _, err = w.FundOrder(ord) 809 if !errors.Is(err, asset.ErrInsufficientBalance) { 810 t.Fatalf("wrong error for reserves rejection: %v", err) 811 } 812 813 // Double-check that we're still good 814 w.reserves.Store(0) 815 queueSuccess() 816 _, _, _, err = w.FundOrder(ord) 817 if err != nil { 818 t.Fatalf("got out of whack somehow") 819 } 820 w.prepareCoinManager() 821 822 // No reserves, no little UTXO, but shielded funds available for 823 // shielded split 824 unspents = []*btc.ListUnspentResult{lottaUTXO} 825 splitOutputAmt := dexzec.RequiredOrderFunds(ord.Value, 1, dexbtc.RedeemP2PKHInputSize, ord.MaxSwapCount) 826 shieldedSplitFees := dexzec.TxFeesZIP317(dexbtc.RedeemP2PKHInputSize+1, dexbtc.P2PKHOutputSize+1, 0, 0, 0, nActionsOrchardEstimate) 827 w.lastAddress.Store(tUnifiedAddr) 828 acctBal.Pools.Orchard.ValueZat = splitOutputAmt + shieldedSplitFees - lottaFunds 829 queueSuccess() 830 cl.queueResponse(methodZGetAddressForAccount, &zGetAddressForAccountResult{Address: tAddr}) // split output 831 cl.queueResponse(methodZListUnifiedReceivers, &unifiedReceivers{Transparent: tAddr}) 832 cl.queueResponse(methodZSendMany, "opid-123456") 833 txid := dex.Bytes(encode.RandomBytes(32)).String() 834 cl.queueResponse(methodZGetOperationResult, []*operationStatus{{ 835 Status: "success", 836 Result: &opResult{ 837 TxID: txid, 838 }, 839 }}) 840 btcAddr, _ := dexzec.DecodeAddress(tAddr, w.addrParams, w.btcParams) 841 pkScript, _ := txscript.PayToAddrScript(btcAddr) 842 msgTx := wire.NewMsgTx(dexzec.VersionNU5) 843 msgTx.TxOut = append(msgTx.TxOut, wire.NewTxOut(int64(splitOutputAmt), pkScript)) 844 txB, _ = dexzec.NewTxFromMsgTx(msgTx, dexzec.MaxExpiryHeight).Bytes() 845 cl.getTransactionMap = map[string]*btc.GetTransactionResult{ 846 txid: { 847 Bytes: txB, 848 }, 849 } 850 coins, _, fees, err = w.FundOrder(ord) 851 if err != nil { 852 t.Fatalf("error for shielded split: %v", err) 853 } 854 if len(coins) != 1 { 855 t.Fatalf("shielded split failed - coin count %d != 1", len(coins)) 856 } 857 if fees != shieldedSplitFees { 858 t.Fatalf("shielded split returned unexpected fees. wanted %d, got %d", shieldedSplitFees, fees) 859 } 860 } 861 862 func TestFundingCoins(t *testing.T) { 863 w, cl, shutdown := tNewWallet() 864 defer shutdown() 865 defer cl.checkEmptiness(t) 866 867 const vout0 = 1 868 const txBlockHeight = 3 869 tx0 := makeRawTx([]dex.Bytes{{0x01}, tP2PKH}, []*wire.TxIn{dummyInput()}) 870 txHash0 := tx0.TxHash() 871 _, _ = cl.addRawTx(txBlockHeight, tx0) 872 coinID0 := btc.ToCoinID(&txHash0, vout0) 873 // Make spendable (confs > 0) 874 cl.addRawTx(txBlockHeight+1, dummyTx()) 875 876 p2pkhUnspent0 := &btc.ListUnspentResult{ 877 TxID: txHash0.String(), 878 Vout: vout0, 879 ScriptPubKey: tP2PKH, 880 Spendable: true, 881 Solvable: true, 882 SafePtr: boolPtr(true), 883 Amount: 1, 884 } 885 unspents := []*btc.ListUnspentResult{p2pkhUnspent0} 886 887 // Add a second funding coin to make sure more than one iteration of the 888 // utxo loops is required. 889 const vout1 = 0 890 tx1 := makeRawTx([]dex.Bytes{tP2PKH, {0x02}}, []*wire.TxIn{dummyInput()}) 891 txHash1 := tx1.TxHash() 892 _, _ = cl.addRawTx(txBlockHeight, tx1) 893 coinID1 := btc.ToCoinID(&txHash1, vout1) 894 // Make spendable (confs > 0) 895 cl.addRawTx(txBlockHeight+1, dummyTx()) 896 897 p2pkhUnspent1 := &btc.ListUnspentResult{ 898 TxID: txHash1.String(), 899 Vout: vout1, 900 ScriptPubKey: tP2PKH, 901 Spendable: true, 902 Solvable: true, 903 SafePtr: boolPtr(true), 904 Amount: 1, 905 } 906 unspents = append(unspents, p2pkhUnspent1) 907 coinIDs := []dex.Bytes{coinID0, coinID1} 908 locked := []*btc.RPCOutpoint{} 909 910 queueSuccess := func() { 911 cl.queueResponse("listlockunspent", locked) 912 cl.queueResponse("listunspent", unspents) 913 cl.queueResponse("lockunspent", true) 914 } 915 916 ensureGood := func() { 917 t.Helper() 918 coins, err := w.FundingCoins(coinIDs) 919 if err != nil { 920 t.Fatalf("FundingCoins error: %v", err) 921 } 922 if len(coins) != 2 { 923 t.Fatalf("expected 2 coins, got %d", len(coins)) 924 } 925 } 926 queueSuccess() 927 ensureGood() 928 929 ensureErr := func(tag string) { 930 t.Helper() 931 // Clear the cache. 932 w.prepareCoinManager() 933 _, err := w.FundingCoins(coinIDs) 934 if err == nil { 935 t.Fatalf("%s: no error", tag) 936 } 937 } 938 939 // No coins 940 cl.queueResponse("listlockunspent", locked) 941 cl.queueResponse("listunspent", []*btc.ListUnspentResult{}) 942 ensureErr("no coins") 943 944 // RPC error 945 cl.queueResponse("listlockunspent", tErr) 946 ensureErr("rpc coins") 947 948 // Bad coin ID. 949 goodCoinIDs := coinIDs 950 coinIDs = []dex.Bytes{encode.RandomBytes(35)} 951 ensureErr("bad coin ID") 952 coinIDs = goodCoinIDs 953 954 queueSuccess() 955 ensureGood() 956 } 957 958 func TestSwap(t *testing.T) { 959 w, cl, shutdown := tNewWallet() 960 defer shutdown() 961 defer cl.checkEmptiness(t) 962 963 swapVal := toZats(5) 964 coins := asset.Coins{ 965 btc.NewOutput(tTxHash, 0, toZats(3)), 966 btc.NewOutput(tTxHash, 0, toZats(3)), 967 } 968 addrStr := tAddr 969 970 privBytes, _ := hex.DecodeString("b07209eec1a8fb6cfe5cb6ace36567406971a75c330db7101fb21bc679bc5330") 971 privKey, _ := btcec.PrivKeyFromBytes(privBytes) 972 wif, _ := btcutil.NewWIF(privKey, &chaincfg.MainNetParams, true) 973 974 secretHash, _ := hex.DecodeString("5124208c80d33507befa517c08ed01aa8d33adbf37ecd70fb5f9352f7a51a88d") 975 contract := &asset.Contract{ 976 Address: addrStr, 977 Value: swapVal, 978 SecretHash: secretHash, 979 LockTime: uint64(time.Now().Unix()), 980 } 981 982 swaps := &asset.Swaps{ 983 Inputs: coins, 984 Contracts: []*asset.Contract{contract}, 985 LockChange: true, 986 } 987 988 var rawSent bool 989 queueSuccess := func() { 990 cl.queueResponse(methodZGetAddressForAccount, &zGetAddressForAccountResult{Address: tAddr}) // revocation output 991 cl.queueResponse(methodZListUnifiedReceivers, &unifiedReceivers{Transparent: tAddr}) 992 cl.queueResponse(methodZGetAddressForAccount, &zGetAddressForAccountResult{Address: tAddr}) // change output 993 cl.queueResponse(methodZListUnifiedReceivers, &unifiedReceivers{Transparent: tAddr}) 994 cl.queueResponse("dumpprivkey", wif.String()) 995 cl.queueResponse("sendrawtransaction", func(args []json.RawMessage) (json.RawMessage, error) { 996 rawSent = true 997 tx, _ := signRawJSONTx(args[0]) 998 return json.Marshal(tx.TxHash().String()) 999 }) 1000 } 1001 1002 // This time should succeed. 1003 queueSuccess() 1004 _, _, feesPaid, err := w.Swap(swaps) 1005 if err != nil { 1006 t.Fatalf("swap error: %v", err) 1007 } 1008 if !rawSent { 1009 t.Fatalf("tx not sent?") 1010 } 1011 1012 // Fees should be returned. 1013 btcAddr, _ := dexzec.DecodeAddress(tAddr, w.addrParams, w.btcParams) 1014 contractScript, err := dexbtc.MakeContract(btcAddr, btcAddr, encode.RandomBytes(32), 0, false, w.btcParams) 1015 if err != nil { 1016 t.Fatalf("unable to create pubkey script for address %s: %v", contract.Address, err) 1017 } 1018 // contracts = append(contracts, contractScript) 1019 1020 // Make the P2SH address and pubkey script. 1021 scriptAddr, err := w.scriptHashAddress(contractScript) 1022 if err != nil { 1023 t.Fatalf("error encoding script address: %v", err) 1024 } 1025 1026 pkScript, err := txscript.PayToAddrScript(scriptAddr) 1027 if err != nil { 1028 t.Fatalf("error creating pubkey script: %v", err) 1029 } 1030 txOutsSize := uint64(wire.NewTxOut(int64(contract.Value), pkScript).SerializeSize()) 1031 txInsSize := uint64(len(coins))*dexbtc.RedeemP2PKHInputSize + 1 1032 fees := dexzec.TxFeesZIP317(txInsSize, txOutsSize, 0, 0, 0, 0) 1033 if feesPaid != fees { 1034 t.Fatalf("sent fees, %d, less than required fees, %d", feesPaid, fees) 1035 } 1036 1037 // Not enough funds 1038 swaps.Inputs = coins[:1] 1039 cl.queueResponse(methodZGetAddressForAccount, &zGetAddressForAccountResult{Address: tAddr}) // change address 1040 cl.queueResponse(methodZListUnifiedReceivers, &unifiedReceivers{Transparent: tAddr}) 1041 _, _, _, err = w.Swap(swaps) 1042 if !errorHasCode(err, errInsufficientBalance) { 1043 t.Fatalf("wrong error for listunspent not enough funds: %v", err) 1044 } 1045 swaps.Inputs = coins 1046 1047 // Make sure we can succeed again. 1048 queueSuccess() 1049 _, _, _, err = w.Swap(swaps) 1050 if err != nil { 1051 t.Fatalf("re-swap error: %v", err) 1052 } 1053 } 1054 1055 func TestRedeem(t *testing.T) { 1056 w, cl, shutdown := tNewWallet() 1057 defer shutdown() 1058 defer cl.checkEmptiness(t) 1059 swapVal := toZats(5) 1060 1061 secret, _, _, contract, _, _, lockTime := makeSwapContract(time.Hour * 12) 1062 1063 coin := btc.NewOutput(tTxHash, 0, swapVal) 1064 ci := &asset.AuditInfo{ 1065 Coin: coin, 1066 Contract: contract, 1067 Recipient: tAddr, 1068 Expiration: lockTime, 1069 } 1070 1071 redemption := &asset.Redemption{ 1072 Spends: ci, 1073 Secret: secret, 1074 } 1075 1076 privBytes, _ := hex.DecodeString("b07209eec1a8fb6cfe5cb6ace36567406971a75c330db7101fb21bc679bc5330") 1077 privKey, _ := btcec.PrivKeyFromBytes(privBytes) 1078 wif, err := btcutil.NewWIF(privKey, &chaincfg.RegressionNetParams, true) 1079 if err != nil { 1080 t.Fatalf("error encoding wif: %v", err) 1081 } 1082 1083 redemptions := &asset.RedeemForm{ 1084 Redemptions: []*asset.Redemption{redemption}, 1085 } 1086 1087 cl.queueResponse(methodZGetAddressForAccount, &zGetAddressForAccountResult{Address: tAddr}) // revocation output 1088 cl.queueResponse(methodZListUnifiedReceivers, &unifiedReceivers{Transparent: tAddr}) 1089 cl.queueResponse("dumpprivkey", wif.String()) 1090 var rawSent bool 1091 cl.queueResponse("sendrawtransaction", func(args []json.RawMessage) (json.RawMessage, error) { 1092 rawSent = true 1093 tx, _ := signRawJSONTx(args[0]) 1094 return json.Marshal(tx.TxHash().String()) 1095 }) 1096 1097 _, _, _, err = w.Redeem(redemptions) 1098 if err != nil { 1099 t.Fatalf("redeem error: %v", err) 1100 } 1101 if !rawSent { 1102 t.Fatalf("split failed - no tx sent") 1103 } 1104 } 1105 func TestSend(t *testing.T) { 1106 w, cl, shutdown := tNewWallet() 1107 defer shutdown() 1108 defer cl.checkEmptiness(t) 1109 1110 const sendAmt uint64 = 1e8 1111 w.lastAddress.Store(tUnifiedAddr) 1112 1113 successOp := []*operationStatus{{ 1114 Status: "success", 1115 Result: &opResult{TxID: tTxID}, 1116 }} 1117 1118 dTx := dummyTx() 1119 txB, _ := dTx.Bytes() 1120 tx := &btc.GetTransactionResult{ 1121 Bytes: txB, 1122 } 1123 1124 // Unified address. 1125 cl.queueResponse(methodZValidateAddress, &zValidateAddressResult{IsValid: true, AddressType: unifiedAddressType}) 1126 cl.queueResponse(methodZListUnifiedReceivers, &unifiedReceivers{Transparent: tAddr}) 1127 cl.queueResponse(methodZSendMany, "operationid123") 1128 cl.queueResponse(methodZGetOperationResult, successOp) 1129 cl.queueResponse("gettransaction", tx) 1130 if _, err := w.Send(tUnifiedAddr, sendAmt, 0); err != nil { 1131 t.Fatalf("Send error: %v", err) 1132 } 1133 1134 // Transparent address. 1135 cl.queueResponse(methodZValidateAddress, &zValidateAddressResult{IsValid: true, AddressType: transparentAddressType}) 1136 cl.queueResponse(methodZSendMany, "operationid123") 1137 cl.queueResponse(methodZGetOperationResult, successOp) 1138 cl.queueResponse("gettransaction", tx) 1139 if _, err := w.Send(tAddr, sendAmt, 0); err != nil { 1140 t.Fatalf("Send error: %v", err) 1141 } 1142 } 1143 1144 func TestSwapConfirmations(t *testing.T) { 1145 w, cl, shutdown := tNewWallet() 1146 defer shutdown() 1147 defer cl.checkEmptiness(t) 1148 1149 _, _, pkScript, contract, _, _, _ := makeSwapContract(time.Hour * 12) 1150 const tipHeight = 10 1151 const swapHeight = 2 1152 const expConfs = tipHeight - swapHeight + 1 1153 1154 tx := makeRawTx([]dex.Bytes{pkScript}, []*wire.TxIn{dummyInput()}) 1155 txB, _ := tx.Bytes() 1156 blockHash, swapBlock := cl.addRawTx(swapHeight, tx) 1157 txHash := tx.TxHash() 1158 cl.getTransactionMap = map[string]*btc.GetTransactionResult{ 1159 txHash.String(): { 1160 Bytes: txB, 1161 }, 1162 } 1163 coinID := btc.ToCoinID(&txHash, 0) 1164 // Simulate a spending transaction, and advance the tip so that the swap 1165 // has two confirmations. 1166 spendingTx := dummyTx() 1167 spendingTx.TxIn[0].PreviousOutPoint.Hash = txHash 1168 1169 // Prime the blockchain 1170 for i := int64(1); i <= tipHeight; i++ { 1171 cl.addRawTx(i, dummyTx()) 1172 } 1173 1174 matchTime := swapBlock.Header.Timestamp 1175 1176 // Bad coin id 1177 _, _, err := w.SwapConfirmations(tCtx, encode.RandomBytes(35), contract, matchTime) 1178 if err == nil { 1179 t.Fatalf("no error for bad coin ID") 1180 } 1181 1182 txOutRes := &btcjson.GetTxOutResult{ 1183 Confirmations: expConfs, 1184 BestBlock: blockHash.String(), 1185 } 1186 1187 cl.queueResponse("gettxout", txOutRes) 1188 confs, _, err := w.SwapConfirmations(tCtx, coinID, contract, matchTime) 1189 if err != nil { 1190 t.Fatalf("error for gettransaction path: %v", err) 1191 } 1192 if confs != expConfs { 1193 t.Fatalf("confs not retrieved from gettxout path. expected %d, got %d", expConfs, confs) 1194 } 1195 1196 // no tx output found 1197 cl.queueResponse("gettxout", nil) 1198 cl.queueResponse("gettransaction", tErr) 1199 _, _, err = w.SwapConfirmations(tCtx, coinID, contract, matchTime) 1200 if !errorHasCode(err, errNoTx) { 1201 t.Fatalf("wrong error for gettransaction error: %v", err) 1202 } 1203 1204 cl.queueResponse("gettxout", nil) 1205 cl.queueResponse("gettransaction", &dcrjson.RPCError{ 1206 Code: dcrjson.RPCErrorCode(btcjson.ErrRPCNoTxInfo), 1207 }) 1208 _, _, err = w.SwapConfirmations(tCtx, coinID, contract, matchTime) 1209 if !errors.Is(err, asset.CoinNotFoundError) { 1210 t.Fatalf("wrong error for CoinNotFoundError: %v", err) 1211 } 1212 1213 cl.queueResponse("gettxout", nil) 1214 // Will pull transaction from getTransactionMap 1215 _, _, err = w.SwapConfirmations(tCtx, coinID, contract, matchTime) 1216 if err != nil { 1217 t.Fatalf("error for gettransaction path: %v", err) 1218 } 1219 } 1220 1221 func TestSyncStatus(t *testing.T) { 1222 w, cl, shutdown := tNewWallet() 1223 defer shutdown() 1224 defer cl.checkEmptiness(t) 1225 1226 bci := &btc.GetBlockchainInfoResult{ 1227 Headers: 100, 1228 Blocks: 99, // full node allowed to be synced when 1 block behind 1229 } 1230 1231 const numConns = 2 1232 ni := &networkInfo{ 1233 Connections: numConns, 1234 } 1235 1236 // ok 1237 cl.queueResponse("getblockchaininfo", bci) 1238 cl.queueResponse("getnetworkinfo", ni) 1239 ss, err := w.SyncStatus() 1240 if err != nil { 1241 t.Fatalf("SyncStatus error (synced expected): %v", err) 1242 } 1243 if !ss.Synced { 1244 t.Fatalf("synced = false") 1245 } 1246 if ss.BlockProgress() < 1 { 1247 t.Fatalf("progress not complete when loading last block") 1248 } 1249 1250 // getblockchaininfo error 1251 cl.queueResponse("getblockchaininfo", tErr) 1252 _, err = w.SyncStatus() 1253 if !errorHasCode(err, errGetChainInfo) { 1254 t.Fatalf("SyncStatus wrong error: %v", err) 1255 } 1256 1257 // getnetworkinfo error 1258 cl.queueResponse("getblockchaininfo", bci) 1259 cl.queueResponse("getnetworkinfo", tErr) 1260 _, err = w.SyncStatus() 1261 if !errorHasCode(err, errGetNetInfo) { 1262 t.Fatalf("SyncStatus wrong error: %v", err) 1263 } 1264 1265 // no peers is not synced = false 1266 ni.Connections = 0 1267 cl.queueResponse("getblockchaininfo", bci) 1268 cl.queueResponse("getnetworkinfo", ni) 1269 ss, err = w.SyncStatus() 1270 if err != nil { 1271 t.Fatalf("SyncStatus error (!synced expected): %v", err) 1272 } 1273 if ss.Synced { 1274 t.Fatalf("synced = true") 1275 } 1276 ni.Connections = 2 1277 1278 // No headers is progress = 0 1279 bci.Headers = 0 1280 cl.queueResponse("getblockchaininfo", bci) 1281 ss, err = w.SyncStatus() 1282 if err != nil { 1283 t.Fatalf("SyncStatus error (no headers): %v", err) 1284 } 1285 if ss.Synced || ss.BlockProgress() != 0 { 1286 t.Fatal("wrong sync status for no headers", ss.Synced, ss.BlockProgress()) 1287 } 1288 bci.Headers = 100 1289 1290 // 50% synced 1291 w.tipAtConnect.Store(100) 1292 bci.Headers = 200 1293 bci.Blocks = 150 1294 cl.queueResponse("getblockchaininfo", bci) 1295 ss, err = w.SyncStatus() 1296 if err != nil { 1297 t.Fatalf("SyncStatus error (half-synced): %v", err) 1298 } 1299 if ss.Synced { 1300 t.Fatalf("synced = true for 50 blocks to go") 1301 } 1302 if ss.BlockProgress() > 0.500001 || ss.BlockProgress() < 0.4999999 { 1303 t.Fatalf("progress out of range. Expected 0.5, got %.2f", ss.BlockProgress()) 1304 } 1305 } 1306 1307 func TestPreSwap(t *testing.T) { 1308 w, cl, shutdown := tNewWallet() 1309 defer shutdown() 1310 defer cl.checkEmptiness(t) 1311 1312 // See math from TestFundEdges. 10 lots with max fee rate of 34 sats/vbyte. 1313 1314 const lots = 10 1315 swapVal := lots * tLotSize 1316 1317 singleSwapFees := dexzec.TxFeesZIP317(dexbtc.RedeemP2PKHInputSize+1, dexbtc.P2SHOutputSize+1, 0, 0, 0, 0) 1318 bestCaseFees := singleSwapFees 1319 worstCaseFees := lots * bestCaseFees 1320 1321 minReq := swapVal + worstCaseFees 1322 1323 unspent := &btc.ListUnspentResult{ 1324 TxID: tTxID, 1325 Address: tAddr, 1326 Confirmations: 5, 1327 ScriptPubKey: tP2PKH, 1328 Spendable: true, 1329 Solvable: true, 1330 } 1331 unspents := []*btc.ListUnspentResult{unspent} 1332 1333 setFunds := func(v uint64) { 1334 unspent.Amount = float64(v) / 1e8 1335 } 1336 1337 form := &asset.PreSwapForm{ 1338 Version: version, 1339 LotSize: tLotSize, 1340 Lots: lots, 1341 Immediate: false, 1342 } 1343 1344 setFunds(minReq) 1345 1346 acctBal := &zAccountBalance{} 1347 queueFunding := func() { 1348 cl.queueResponse("listunspent", unspents) // maxOrder 1349 cl.queueResponse(methodZGetBalanceForAccount, acctBal) // 0-conf 1350 cl.queueResponse(methodZGetBalanceForAccount, acctBal) // minOrchardConfs 1351 cl.queueResponse(methodZGetNotesCount, &zNotesCount{Orchard: 1}) 1352 } 1353 1354 // Initial success. 1355 queueFunding() 1356 preSwap, err := w.PreSwap(form) 1357 if err != nil { 1358 t.Fatalf("PreSwap error: %v", err) 1359 } 1360 1361 if preSwap.Estimate.Lots != lots { 1362 t.Fatalf("wrong lots. expected %d got %d", lots, preSwap.Estimate.Lots) 1363 } 1364 if preSwap.Estimate.MaxFees != worstCaseFees { 1365 t.Fatalf("wrong worst-case fees. expected %d got %d", worstCaseFees, preSwap.Estimate.MaxFees) 1366 } 1367 if preSwap.Estimate.RealisticBestCase != bestCaseFees { 1368 t.Fatalf("wrong best-case fees. expected %d got %d", bestCaseFees, preSwap.Estimate.RealisticBestCase) 1369 } 1370 w.prepareCoinManager() 1371 1372 // Too little funding is an error. 1373 setFunds(minReq - 1) 1374 cl.queueResponse("listunspent", unspents) // maxOrder 1375 cl.queueResponse(methodZGetBalanceForAccount, &zAccountBalance{ 1376 Pools: zBalancePools{ 1377 Transparent: valZat{ 1378 ValueZat: toZats(unspent.Amount), 1379 }, 1380 }, 1381 }) 1382 _, err = w.PreSwap(form) 1383 if err == nil { 1384 t.Fatalf("no PreSwap error for not enough funds") 1385 } 1386 setFunds(minReq) 1387 1388 // Success again. 1389 queueFunding() 1390 _, err = w.PreSwap(form) 1391 if err != nil { 1392 t.Fatalf("final PreSwap error: %v", err) 1393 } 1394 } 1395 1396 func TestConfirmRedemption(t *testing.T) { 1397 w, cl, shutdown := tNewWallet() 1398 defer shutdown() 1399 defer cl.checkEmptiness(t) 1400 1401 swapVal := toZats(5) 1402 1403 secret, _, _, contract, addr, _, lockTime := makeSwapContract(time.Hour * 12) 1404 1405 coin := btc.NewOutput(tTxHash, 0, swapVal) 1406 coinID := coin.ID() 1407 ci := &asset.AuditInfo{ 1408 Coin: coin, 1409 Contract: contract, 1410 Recipient: addr.String(), 1411 Expiration: lockTime, 1412 } 1413 1414 redemption := &asset.Redemption{ 1415 Spends: ci, 1416 Secret: secret, 1417 } 1418 1419 walletTx := &btc.GetTransactionResult{ 1420 Confirmations: 1, 1421 } 1422 1423 cl.queueResponse("gettransaction", walletTx) 1424 st, err := w.ConfirmRedemption(coinID, redemption, 0) 1425 if err != nil { 1426 t.Fatalf("Initial ConfirmRedemption error: %v", err) 1427 } 1428 if st.Confs != walletTx.Confirmations { 1429 t.Fatalf("wrongs confs, %d != %d", st.Confs, walletTx.Confirmations) 1430 } 1431 1432 cl.queueResponse("gettransaction", tErr) 1433 cl.queueResponse("gettxout", tErr) 1434 _, err = w.ConfirmRedemption(coinID, redemption, 0) 1435 if !errorHasCode(err, errNoTx) { 1436 t.Fatalf("wrong error for gettxout error: %v", err) 1437 } 1438 1439 cl.queueResponse("gettransaction", tErr) 1440 cl.queueResponse("gettxout", nil) 1441 st, err = w.ConfirmRedemption(coinID, redemption, 0) 1442 if err != nil { 1443 t.Fatalf("ConfirmRedemption error for spent redemption: %v", err) 1444 } 1445 if st.Confs != requiredRedeemConfirms { 1446 t.Fatalf("wrong confs for spent redemption: %d != %d", st.Confs, requiredRedeemConfirms) 1447 } 1448 1449 // Re-submission path is tested by TestRedemption 1450 } 1451 1452 func TestFundMultiOrder(t *testing.T) { 1453 w, cl, shutdown := tNewWallet() 1454 defer shutdown() 1455 defer cl.checkEmptiness(t) 1456 1457 const maxLock = 1e16 1458 1459 // Invalid input 1460 mo := &asset.MultiOrder{ 1461 Values: []*asset.MultiOrderValue{{}}, // 1 zero-value order 1462 } 1463 if _, _, _, err := w.FundMultiOrder(mo, maxLock); !errorHasCode(err, errBadInput) { 1464 t.Fatalf("wrong error for zero value: %v", err) 1465 } 1466 1467 mo = &asset.MultiOrder{ 1468 Values: []*asset.MultiOrderValue{{ 1469 Value: tLotSize, 1470 MaxSwapCount: 1, 1471 }}, 1472 } 1473 req := dexzec.RequiredOrderFunds(tLotSize, 1, dexbtc.RedeemP2PKHInputSize, 1) 1474 1475 // maxLock too low 1476 if _, _, _, err := w.FundMultiOrder(mo, 1); !errorHasCode(err, errMaxLock) { 1477 t.Fatalf("wrong error for exceeding maxLock: %v", err) 1478 } 1479 1480 // Balance error 1481 cl.queueResponse(methodZGetBalanceForAccount, tErr) 1482 if _, _, _, err := w.FundMultiOrder(mo, maxLock); !errorHasCode(err, errFunding) { 1483 t.Fatalf("wrong error for expected balance error: %v", err) 1484 } 1485 1486 acctBal := &zAccountBalance{ 1487 Pools: zBalancePools{ 1488 Transparent: valZat{ 1489 ValueZat: req - 1, // too little 1490 }, 1491 }, 1492 } 1493 1494 queueBalance := func() { 1495 cl.queueResponse(methodZGetBalanceForAccount, acctBal) // 0-conf 1496 cl.queueResponse(methodZGetBalanceForAccount, acctBal) // minOrchardConfs 1497 cl.queueResponse(methodZGetNotesCount, &zNotesCount{Orchard: 1}) 1498 cl.queueResponse("listlockunspent", nil) 1499 } 1500 1501 // Not enough balance 1502 queueBalance() 1503 if _, _, _, err := w.FundMultiOrder(mo, maxLock); !errorHasCode(err, errInsufficientBalance) { 1504 t.Fatalf("wrong low balance error: %v", err) 1505 } 1506 1507 // listunspent error 1508 acctBal.Pools.Transparent.ValueZat = req 1509 queueBalance() 1510 cl.queueResponse("listunspent", tErr) 1511 if _, _, _, err := w.FundMultiOrder(mo, maxLock); !errorHasCode(err, errFunding) { 1512 t.Fatalf("wrong error for listunspent error: %v", err) 1513 } 1514 1515 // Enough without split. 1516 unspent := &btc.ListUnspentResult{ 1517 ScriptPubKey: tP2PKH, 1518 Amount: float64(req) / 1e8, 1519 TxID: tTxID, 1520 Confirmations: 1, 1521 Spendable: true, 1522 } 1523 unspents := []*btc.ListUnspentResult{unspent} 1524 1525 queueBalance() 1526 cl.queueResponse("listunspent", unspents) 1527 if _, _, _, err := w.FundMultiOrder(mo, maxLock); err != nil { 1528 t.Fatalf("error for simple path: %v", err) 1529 } 1530 }