|
1 | 1 | package chain
|
2 | 2 |
|
3 | 3 | import (
|
| 4 | + "bytes" |
4 | 5 | "container/list"
|
5 | 6 | "encoding/hex"
|
| 7 | + "encoding/json" |
6 | 8 | "errors"
|
7 | 9 | "fmt"
|
8 | 10 | "sync"
|
@@ -1335,6 +1337,130 @@ func (c *BitcoindClient) LookupInputMempoolSpend(op wire.OutPoint) (
|
1335 | 1337 | return c.chainConn.events.LookupInputSpend(op)
|
1336 | 1338 | }
|
1337 | 1339 |
|
| 1340 | +// SubmitPackage broadcasts a parents+child package atomically. |
| 1341 | +// |
| 1342 | +// `parents` must contain at least one transaction. The returned slice |
| 1343 | +// holds the txids of every transaction that entered the mempool. |
| 1344 | +// |
| 1345 | +// `maxFeeRateBTCPerVByte` caps the effective feerate the node will |
| 1346 | +// accept for the entire package, expressed in BTC per virtual byte. |
| 1347 | +// Pass nil to leave the limit unset. |
| 1348 | +// |
| 1349 | +// If the active backend cannot relay packages, ErrUnimplemented is |
| 1350 | +// returned. |
| 1351 | +func (c *BitcoindClient) SubmitPackage(parents []*wire.MsgTx, child *wire.MsgTx, |
| 1352 | + maxFeeRateBTCPerVByte *float64) ([]*chainhash.Hash, error) { |
| 1353 | + |
| 1354 | + // Sanity check inputs. |
| 1355 | + if len(parents) == 0 { |
| 1356 | + return nil, errors.New("submitpackage: need at least one " + |
| 1357 | + "parent txn") |
| 1358 | + } |
| 1359 | + |
| 1360 | + if child == nil { |
| 1361 | + return nil, errors.New("submitpackage: child txn not defined") |
| 1362 | + } |
| 1363 | + |
| 1364 | + // Prepare the hex encoded transactions to that we'll add the the |
| 1365 | + // request. |
| 1366 | + toHex := func(tx *wire.MsgTx) (string, error) { |
| 1367 | + var buf bytes.Buffer |
| 1368 | + if err := tx.Serialize(&buf); err != nil { |
| 1369 | + return "", err |
| 1370 | + } |
| 1371 | + |
| 1372 | + return hex.EncodeToString(buf.Bytes()), nil |
| 1373 | + } |
| 1374 | + |
| 1375 | + rawTxs := make([]string, 0, len(parents)+1) |
| 1376 | + |
| 1377 | + for i, ch := range parents { |
| 1378 | + h, err := toHex(ch) |
| 1379 | + if err != nil { |
| 1380 | + return nil, fmt.Errorf("cannot serialize parent txn "+ |
| 1381 | + "%d: %w", i, err) |
| 1382 | + } |
| 1383 | + |
| 1384 | + rawTxs = append(rawTxs, h) |
| 1385 | + } |
| 1386 | + |
| 1387 | + h, err := toHex(child) |
| 1388 | + if err != nil { |
| 1389 | + return nil, fmt.Errorf("cannot serialize child txn: %w", err) |
| 1390 | + } |
| 1391 | + |
| 1392 | + rawTxs = append(rawTxs, h) |
| 1393 | + |
| 1394 | + // Parameter 1: The package (array of hex strings). |
| 1395 | + rawTxsJSON, err := json.Marshal(rawTxs) |
| 1396 | + if err != nil { |
| 1397 | + return nil, fmt.Errorf("failed to marshal raw txs array: %w", |
| 1398 | + err) |
| 1399 | + } |
| 1400 | + |
| 1401 | + params := []json.RawMessage{rawTxsJSON} |
| 1402 | + |
| 1403 | + // Parameter 2: maxfeerate (optional, numeric, in BTC/kvB) |
| 1404 | + if maxFeeRateBTCPerVByte != nil { |
| 1405 | + // Convert BTC/vByte to BTC/kvB. |
| 1406 | + maxFeeRateBTCPerKvB := *maxFeeRateBTCPerVByte * 1000 |
| 1407 | + maxFeeRateJSON, err := json.Marshal(maxFeeRateBTCPerKvB) |
| 1408 | + if err != nil { |
| 1409 | + return nil, fmt.Errorf("failed to marshal "+ |
| 1410 | + "maxfeerate: %w", err) |
| 1411 | + } |
| 1412 | + |
| 1413 | + params = append(params, maxFeeRateJSON) |
| 1414 | + } |
| 1415 | + |
| 1416 | + resp, err := c.chainConn.client.RawRequest("submitpackage", params) |
| 1417 | + if err != nil { |
| 1418 | + if isMethodNotFound(err) { |
| 1419 | + return nil, ErrUnimplemented |
| 1420 | + } |
| 1421 | + return nil, err |
| 1422 | + } |
| 1423 | + |
| 1424 | + // Unmarshall the response. |
| 1425 | + var result btcjson.SubmitPackageResult |
| 1426 | + if err := json.Unmarshal(resp, &result); err != nil { |
| 1427 | + return nil, err |
| 1428 | + } |
| 1429 | + |
| 1430 | + submittedIDs := make([]*chainhash.Hash, 0, len(result.TxResults)) |
| 1431 | + for _, txRes := range result.TxResults { |
| 1432 | + if txRes.Error != nil { |
| 1433 | + // Skip rejected transactions. |
| 1434 | + continue |
| 1435 | + } |
| 1436 | + |
| 1437 | + if txRes.OtherWtxid != nil { |
| 1438 | + // This is a child transaction that was already |
| 1439 | + // in the mempool. |
| 1440 | + continue |
| 1441 | + } |
| 1442 | + |
| 1443 | + h, err := chainhash.NewHashFromStr(txRes.TxID) |
| 1444 | + if err != nil { |
| 1445 | + // Note that this should never happen. |
| 1446 | + return nil, fmt.Errorf("failed to parse txid '%s' "+ |
| 1447 | + "from submitpackage result: %w", txRes.TxID, |
| 1448 | + err) |
| 1449 | + } |
| 1450 | + |
| 1451 | + submittedIDs = append(submittedIDs, h) |
| 1452 | + } |
| 1453 | + |
| 1454 | + return submittedIDs, nil |
| 1455 | +} |
| 1456 | + |
| 1457 | +// isMethodNotFound returns true if err is exactly the JSON‑RPC "method not |
| 1458 | +// found" error, regardless of the transport. |
| 1459 | +func isMethodNotFound(err error) bool { |
| 1460 | + rpcErr, ok := err.(*btcjson.RPCError) |
| 1461 | + return ok && rpcErr.Code == btcjson.ErrRPCMethodNotFound.Code |
| 1462 | +} |
| 1463 | + |
1338 | 1464 | // resetWatchedFilters empties the maps used to track outpoints, addresses, and
|
1339 | 1465 | // txns.
|
1340 | 1466 | func (c *BitcoindClient) resetWatchedFilters() {
|
|
0 commit comments