github.com/avahowell/sia@v0.5.1-beta.0.20160524050156-83dcc3d37c94/modules/wallet/transactionbuilder.go (about) 1 package wallet 2 3 import ( 4 "bytes" 5 "errors" 6 "sort" 7 8 "github.com/NebulousLabs/Sia/crypto" 9 "github.com/NebulousLabs/Sia/encoding" 10 "github.com/NebulousLabs/Sia/modules" 11 "github.com/NebulousLabs/Sia/types" 12 ) 13 14 var ( 15 // errBuilderAlreadySigned indicates that the transaction builder has 16 // already added at least one successful signature to the transaction, 17 // meaning that future calls to Sign will result in an invalid transaction. 18 errBuilderAlreadySigned = errors.New("sign has already been called on this transaction builder, multiple calls can cause issues") 19 ) 20 21 // transactionBuilder allows transactions to be manually constructed, including 22 // the ability to fund transactions with siacoins and siafunds from the wallet. 23 type transactionBuilder struct { 24 // 'signed' indicates that at least one transaction signature has been 25 // added to the wallet, meaning that future calls to 'Sign' will fail. 26 parents []types.Transaction 27 signed bool 28 transaction types.Transaction 29 30 newParents []int 31 siacoinInputs []int 32 siafundInputs []int 33 transactionSignatures []int 34 35 wallet *Wallet 36 } 37 38 // addSignatures will sign a transaction using a spendable key, with support 39 // for multisig spendable keys. Because of the restricted input, the function 40 // is compatible with both siacoin inputs and siafund inputs. 41 func addSignatures(txn *types.Transaction, cf types.CoveredFields, uc types.UnlockConditions, parentID crypto.Hash, spendKey spendableKey) (newSigIndices []int, err error) { 42 // Try to find the matching secret key for each public key - some public 43 // keys may not have a match. Some secret keys may be used multiple times, 44 // which is why public keys are used as the outer loop. 45 totalSignatures := uint64(0) 46 for i, siaPubKey := range uc.PublicKeys { 47 // Search for the matching secret key to the public key. 48 for j := range spendKey.SecretKeys { 49 pubKey := spendKey.SecretKeys[j].PublicKey() 50 if bytes.Compare(siaPubKey.Key, pubKey[:]) != 0 { 51 continue 52 } 53 54 // Found the right secret key, add a signature. 55 sig := types.TransactionSignature{ 56 ParentID: parentID, 57 CoveredFields: cf, 58 PublicKeyIndex: uint64(i), 59 } 60 newSigIndices = append(newSigIndices, len(txn.TransactionSignatures)) 61 txn.TransactionSignatures = append(txn.TransactionSignatures, sig) 62 sigIndex := len(txn.TransactionSignatures) - 1 63 sigHash := txn.SigHash(sigIndex) 64 encodedSig, err := crypto.SignHash(sigHash, spendKey.SecretKeys[j]) 65 if err != nil { 66 return nil, err 67 } 68 txn.TransactionSignatures[sigIndex].Signature = encodedSig[:] 69 70 // Count that the signature has been added, and break out of the 71 // secret key loop. 72 totalSignatures++ 73 break 74 } 75 76 // If there are enough signatures to satisfy the unlock conditions, 77 // break out of the outer loop. 78 if totalSignatures == uc.SignaturesRequired { 79 break 80 } 81 } 82 return newSigIndices, nil 83 } 84 85 // FundSiacoins will add a siacoin input of exaclty 'amount' to the 86 // transaction. A parent transaction may be needed to achieve an input with the 87 // correct value. The siacoin input will not be signed until 'Sign' is called 88 // on the transaction builder. 89 func (tb *transactionBuilder) FundSiacoins(amount types.Currency) error { 90 tb.wallet.mu.Lock() 91 defer tb.wallet.mu.Unlock() 92 93 // Collect a value-sorted set of siacoin outputs. 94 var so sortedOutputs 95 for scoid, sco := range tb.wallet.siacoinOutputs { 96 so.ids = append(so.ids, scoid) 97 so.outputs = append(so.outputs, sco) 98 } 99 // Add all of the unconfirmed outputs as well. 100 for _, upt := range tb.wallet.unconfirmedProcessedTransactions { 101 for i, sco := range upt.Transaction.SiacoinOutputs { 102 // Determine if the output belongs to the wallet. 103 _, exists := tb.wallet.keys[sco.UnlockHash] 104 if !exists { 105 continue 106 } 107 so.ids = append(so.ids, upt.Transaction.SiacoinOutputID(uint64(i))) 108 so.outputs = append(so.outputs, sco) 109 } 110 } 111 sort.Sort(sort.Reverse(so)) 112 113 // Create and fund a parent transaction that will add the correct amount of 114 // siacoins to the transaction. 115 var fund types.Currency 116 // potentialFund tracks the balance of the wallet including outputs that 117 // have been spent in other unconfirmed transactions recently. This is to 118 // provide the user with a more useful error message in the event that they 119 // are overspending. 120 var potentialFund types.Currency 121 parentTxn := types.Transaction{} 122 var spentScoids []types.SiacoinOutputID 123 for i := range so.ids { 124 scoid := so.ids[i] 125 sco := so.outputs[i] 126 // Check that this output has not recently been spent by the wallet. 127 spendHeight := tb.wallet.spentOutputs[types.OutputID(scoid)] 128 // Prevent an underflow error. 129 allowedHeight := tb.wallet.consensusSetHeight - RespendTimeout 130 if tb.wallet.consensusSetHeight < RespendTimeout { 131 allowedHeight = 0 132 } 133 if spendHeight > allowedHeight { 134 potentialFund = potentialFund.Add(sco.Value) 135 continue 136 } 137 outputUnlockConditions := tb.wallet.keys[sco.UnlockHash].UnlockConditions 138 if tb.wallet.consensusSetHeight < outputUnlockConditions.Timelock { 139 continue 140 } 141 142 // Add a siacoin input for this output. 143 sci := types.SiacoinInput{ 144 ParentID: scoid, 145 UnlockConditions: outputUnlockConditions, 146 } 147 parentTxn.SiacoinInputs = append(parentTxn.SiacoinInputs, sci) 148 spentScoids = append(spentScoids, scoid) 149 150 // Add the output to the total fund 151 fund = fund.Add(sco.Value) 152 potentialFund = potentialFund.Add(sco.Value) 153 if fund.Cmp(amount) >= 0 { 154 break 155 } 156 } 157 if potentialFund.Cmp(amount) >= 0 && fund.Cmp(amount) < 0 { 158 return modules.ErrPotentialDoubleSpend 159 } 160 if fund.Cmp(amount) < 0 { 161 return modules.ErrLowBalance 162 } 163 164 // Create and add the output that will be used to fund the standard 165 // transaction. 166 parentUnlockConditions, err := tb.wallet.nextPrimarySeedAddress() 167 if err != nil { 168 return err 169 } 170 exactOutput := types.SiacoinOutput{ 171 Value: amount, 172 UnlockHash: parentUnlockConditions.UnlockHash(), 173 } 174 parentTxn.SiacoinOutputs = append(parentTxn.SiacoinOutputs, exactOutput) 175 176 // Create a refund output if needed. 177 if amount.Cmp(fund) != 0 { 178 refundUnlockConditions, err := tb.wallet.nextPrimarySeedAddress() 179 if err != nil { 180 return err 181 } 182 refundOutput := types.SiacoinOutput{ 183 Value: fund.Sub(amount), 184 UnlockHash: refundUnlockConditions.UnlockHash(), 185 } 186 parentTxn.SiacoinOutputs = append(parentTxn.SiacoinOutputs, refundOutput) 187 } 188 189 // Sign all of the inputs to the parent trancstion. 190 for _, sci := range parentTxn.SiacoinInputs { 191 _, err := addSignatures(&parentTxn, types.FullCoveredFields, sci.UnlockConditions, crypto.Hash(sci.ParentID), tb.wallet.keys[sci.UnlockConditions.UnlockHash()]) 192 if err != nil { 193 return err 194 } 195 } 196 // Mark the parent output as spent. Must be done after the transaction is 197 // finished because otherwise the txid and output id will change. 198 tb.wallet.spentOutputs[types.OutputID(parentTxn.SiacoinOutputID(0))] = tb.wallet.consensusSetHeight 199 200 // Add the exact output. 201 newInput := types.SiacoinInput{ 202 ParentID: parentTxn.SiacoinOutputID(0), 203 UnlockConditions: parentUnlockConditions, 204 } 205 tb.newParents = append(tb.newParents, len(tb.parents)) 206 tb.parents = append(tb.parents, parentTxn) 207 tb.siacoinInputs = append(tb.siacoinInputs, len(tb.transaction.SiacoinInputs)) 208 tb.transaction.SiacoinInputs = append(tb.transaction.SiacoinInputs, newInput) 209 210 // Mark all outputs that were spent as spent. 211 for _, scoid := range spentScoids { 212 tb.wallet.spentOutputs[types.OutputID(scoid)] = tb.wallet.consensusSetHeight 213 } 214 return nil 215 } 216 217 // FundSiafunds will add a siafund input of exaclty 'amount' to the 218 // transaction. A parent transaction may be needed to achieve an input with the 219 // correct value. The siafund input will not be signed until 'Sign' is called 220 // on the transaction builder. 221 func (tb *transactionBuilder) FundSiafunds(amount types.Currency) error { 222 tb.wallet.mu.Lock() 223 defer tb.wallet.mu.Unlock() 224 225 // Create and fund a parent transaction that will add the correct amount of 226 // siafunds to the transaction. 227 var fund types.Currency 228 var potentialFund types.Currency 229 parentTxn := types.Transaction{} 230 var spentSfoids []types.SiafundOutputID 231 for sfoid, sfo := range tb.wallet.siafundOutputs { 232 // Check that this output has not recently been spent by the wallet. 233 spendHeight := tb.wallet.spentOutputs[types.OutputID(sfoid)] 234 // Prevent an underflow error. 235 allowedHeight := tb.wallet.consensusSetHeight - RespendTimeout 236 if tb.wallet.consensusSetHeight < RespendTimeout { 237 allowedHeight = 0 238 } 239 if spendHeight > allowedHeight { 240 potentialFund = potentialFund.Add(sfo.Value) 241 continue 242 } 243 outputUnlockConditions := tb.wallet.keys[sfo.UnlockHash].UnlockConditions 244 if tb.wallet.consensusSetHeight < outputUnlockConditions.Timelock { 245 continue 246 } 247 248 // Add a siafund input for this output. 249 parentClaimUnlockConditions, err := tb.wallet.nextPrimarySeedAddress() 250 if err != nil { 251 return err 252 } 253 sfi := types.SiafundInput{ 254 ParentID: sfoid, 255 UnlockConditions: outputUnlockConditions, 256 ClaimUnlockHash: parentClaimUnlockConditions.UnlockHash(), 257 } 258 parentTxn.SiafundInputs = append(parentTxn.SiafundInputs, sfi) 259 spentSfoids = append(spentSfoids, sfoid) 260 261 // Add the output to the total fund 262 fund = fund.Add(sfo.Value) 263 potentialFund = potentialFund.Add(sfo.Value) 264 if fund.Cmp(amount) >= 0 { 265 break 266 } 267 } 268 if potentialFund.Cmp(amount) >= 0 && fund.Cmp(amount) < 0 { 269 return modules.ErrPotentialDoubleSpend 270 } 271 if fund.Cmp(amount) < 0 { 272 return modules.ErrLowBalance 273 } 274 275 // Create and add the output that will be used to fund the standard 276 // transaction. 277 parentUnlockConditions, err := tb.wallet.nextPrimarySeedAddress() 278 if err != nil { 279 return err 280 } 281 exactOutput := types.SiafundOutput{ 282 Value: amount, 283 UnlockHash: parentUnlockConditions.UnlockHash(), 284 } 285 parentTxn.SiafundOutputs = append(parentTxn.SiafundOutputs, exactOutput) 286 287 // Create a refund output if needed. 288 if amount.Cmp(fund) != 0 { 289 refundUnlockConditions, err := tb.wallet.nextPrimarySeedAddress() 290 if err != nil { 291 return err 292 } 293 refundOutput := types.SiafundOutput{ 294 Value: fund.Sub(amount), 295 UnlockHash: refundUnlockConditions.UnlockHash(), 296 } 297 parentTxn.SiafundOutputs = append(parentTxn.SiafundOutputs, refundOutput) 298 } 299 300 // Sign all of the inputs to the parent trancstion. 301 for _, sfi := range parentTxn.SiafundInputs { 302 _, err := addSignatures(&parentTxn, types.FullCoveredFields, sfi.UnlockConditions, crypto.Hash(sfi.ParentID), tb.wallet.keys[sfi.UnlockConditions.UnlockHash()]) 303 if err != nil { 304 return err 305 } 306 } 307 308 // Add the exact output. 309 claimUnlockConditions, err := tb.wallet.nextPrimarySeedAddress() 310 if err != nil { 311 return err 312 } 313 newInput := types.SiafundInput{ 314 ParentID: parentTxn.SiafundOutputID(0), 315 UnlockConditions: parentUnlockConditions, 316 ClaimUnlockHash: claimUnlockConditions.UnlockHash(), 317 } 318 tb.newParents = append(tb.newParents, len(tb.parents)) 319 tb.parents = append(tb.parents, parentTxn) 320 tb.siafundInputs = append(tb.siafundInputs, len(tb.transaction.SiafundInputs)) 321 tb.transaction.SiafundInputs = append(tb.transaction.SiafundInputs, newInput) 322 323 // Mark all outputs that were spent as spent. 324 for _, sfoid := range spentSfoids { 325 tb.wallet.spentOutputs[types.OutputID(sfoid)] = tb.wallet.consensusSetHeight 326 } 327 return nil 328 } 329 330 // AddParents adds a set of parents to the transaction. 331 func (tb *transactionBuilder) AddParents(newParents []types.Transaction) { 332 tb.parents = append(tb.parents, newParents...) 333 } 334 335 // AddMinerFee adds a miner fee to the transaction, returning the index of the 336 // miner fee within the transaction. 337 func (tb *transactionBuilder) AddMinerFee(fee types.Currency) uint64 { 338 tb.transaction.MinerFees = append(tb.transaction.MinerFees, fee) 339 return uint64(len(tb.transaction.MinerFees) - 1) 340 } 341 342 // AddSiacoinInput adds a siacoin input to the transaction, returning the index 343 // of the siacoin input within the transaction. When 'Sign' gets called, this 344 // input will be left unsigned. 345 func (tb *transactionBuilder) AddSiacoinInput(input types.SiacoinInput) uint64 { 346 tb.transaction.SiacoinInputs = append(tb.transaction.SiacoinInputs, input) 347 return uint64(len(tb.transaction.SiacoinInputs) - 1) 348 } 349 350 // AddSiacoinOutput adds a siacoin output to the transaction, returning the 351 // index of the siacoin output within the transaction. 352 func (tb *transactionBuilder) AddSiacoinOutput(output types.SiacoinOutput) uint64 { 353 tb.transaction.SiacoinOutputs = append(tb.transaction.SiacoinOutputs, output) 354 return uint64(len(tb.transaction.SiacoinOutputs) - 1) 355 } 356 357 // AddFileContract adds a file contract to the transaction, returning the index 358 // of the file contract within the transaction. 359 func (tb *transactionBuilder) AddFileContract(fc types.FileContract) uint64 { 360 tb.transaction.FileContracts = append(tb.transaction.FileContracts, fc) 361 return uint64(len(tb.transaction.FileContracts) - 1) 362 } 363 364 // AddFileContractRevision adds a file contract revision to the transaction, 365 // returning the index of the file contract revision within the transaction. 366 // When 'Sign' gets called, this revision will be left unsigned. 367 func (tb *transactionBuilder) AddFileContractRevision(fcr types.FileContractRevision) uint64 { 368 tb.transaction.FileContractRevisions = append(tb.transaction.FileContractRevisions, fcr) 369 return uint64(len(tb.transaction.FileContractRevisions) - 1) 370 } 371 372 // AddStorageProof adds a storage proof to the transaction, returning the index 373 // of the storage proof within the transaction. 374 func (tb *transactionBuilder) AddStorageProof(sp types.StorageProof) uint64 { 375 tb.transaction.StorageProofs = append(tb.transaction.StorageProofs, sp) 376 return uint64(len(tb.transaction.StorageProofs) - 1) 377 } 378 379 // AddSiafundInput adds a siafund input to the transaction, returning the index 380 // of the siafund input within the transaction. When 'Sign' is called, this 381 // input will be left unsigned. 382 func (tb *transactionBuilder) AddSiafundInput(input types.SiafundInput) uint64 { 383 tb.transaction.SiafundInputs = append(tb.transaction.SiafundInputs, input) 384 return uint64(len(tb.transaction.SiafundInputs) - 1) 385 } 386 387 // AddSiafundOutput adds a siafund output to the transaction, returning the 388 // index of the siafund output within the transaction. 389 func (tb *transactionBuilder) AddSiafundOutput(output types.SiafundOutput) uint64 { 390 tb.transaction.SiafundOutputs = append(tb.transaction.SiafundOutputs, output) 391 return uint64(len(tb.transaction.SiafundOutputs) - 1) 392 } 393 394 // AddArbitraryData adds arbitrary data to the transaction, returning the index 395 // of the data within the transaction. 396 func (tb *transactionBuilder) AddArbitraryData(arb []byte) uint64 { 397 tb.transaction.ArbitraryData = append(tb.transaction.ArbitraryData, arb) 398 return uint64(len(tb.transaction.ArbitraryData) - 1) 399 } 400 401 // AddTransactionSignature adds a transaction signature to the transaction, 402 // returning the index of the signature within the transaction. The signature 403 // should already be valid, and shouldn't sign any of the inputs that were 404 // added by calling 'FundSiacoins' or 'FundSiafunds'. 405 func (tb *transactionBuilder) AddTransactionSignature(sig types.TransactionSignature) uint64 { 406 tb.transaction.TransactionSignatures = append(tb.transaction.TransactionSignatures, sig) 407 return uint64(len(tb.transaction.TransactionSignatures) - 1) 408 } 409 410 // Drop discards all of the outputs in a transaction, returning them to the 411 // pool so that other transactions may use them. 'Drop' should only be called 412 // if a transaction is both unsigned and will not be used any further. 413 func (tb *transactionBuilder) Drop() { 414 tb.wallet.mu.Lock() 415 defer tb.wallet.mu.Unlock() 416 417 // Iterate through all parents and the transaction itself and restore all 418 // outputs to the list of available outputs. 419 txns := append(tb.parents, tb.transaction) 420 for _, txn := range txns { 421 for _, sci := range txn.SiacoinInputs { 422 delete(tb.wallet.spentOutputs, types.OutputID(sci.ParentID)) 423 } 424 } 425 426 tb.parents = nil 427 tb.signed = false 428 tb.transaction = types.Transaction{} 429 430 tb.newParents = nil 431 tb.siacoinInputs = nil 432 tb.siafundInputs = nil 433 tb.transactionSignatures = nil 434 } 435 436 // Sign will sign any inputs added by 'FundSiacoins' or 'FundSiafunds' and 437 // return a transaction set that contains all parents prepended to the 438 // transaction. If more fields need to be added, a new transaction builder will 439 // need to be created. 440 // 441 // If the whole transaction flag is set to true, then the whole transaction 442 // flag will be set in the covered fields object. If the whole transaction flag 443 // is set to false, then the covered fields object will cover all fields that 444 // have already been added to the transaction, but will also leave room for 445 // more fields to be added. 446 // 447 // Sign should not be called more than once. If, for some reason, there is an 448 // error while calling Sign, the builder should be dropped. 449 func (tb *transactionBuilder) Sign(wholeTransaction bool) ([]types.Transaction, error) { 450 if tb.signed { 451 return nil, errBuilderAlreadySigned 452 } 453 454 // Create the coveredfields struct. 455 var coveredFields types.CoveredFields 456 if wholeTransaction { 457 coveredFields = types.CoveredFields{WholeTransaction: true} 458 } else { 459 for i := range tb.transaction.MinerFees { 460 coveredFields.MinerFees = append(coveredFields.MinerFees, uint64(i)) 461 } 462 for i := range tb.transaction.SiacoinInputs { 463 coveredFields.SiacoinInputs = append(coveredFields.SiacoinInputs, uint64(i)) 464 } 465 for i := range tb.transaction.SiacoinOutputs { 466 coveredFields.SiacoinOutputs = append(coveredFields.SiacoinOutputs, uint64(i)) 467 } 468 for i := range tb.transaction.FileContracts { 469 coveredFields.FileContracts = append(coveredFields.FileContracts, uint64(i)) 470 } 471 for i := range tb.transaction.FileContractRevisions { 472 coveredFields.FileContractRevisions = append(coveredFields.FileContractRevisions, uint64(i)) 473 } 474 for i := range tb.transaction.StorageProofs { 475 coveredFields.StorageProofs = append(coveredFields.StorageProofs, uint64(i)) 476 } 477 for i := range tb.transaction.SiafundInputs { 478 coveredFields.SiafundInputs = append(coveredFields.SiafundInputs, uint64(i)) 479 } 480 for i := range tb.transaction.SiafundOutputs { 481 coveredFields.SiafundOutputs = append(coveredFields.SiafundOutputs, uint64(i)) 482 } 483 for i := range tb.transaction.ArbitraryData { 484 coveredFields.ArbitraryData = append(coveredFields.ArbitraryData, uint64(i)) 485 } 486 } 487 // TransactionSignatures don't get covered by the 'WholeTransaction' flag, 488 // and must be covered manually. 489 for i := range tb.transaction.TransactionSignatures { 490 coveredFields.TransactionSignatures = append(coveredFields.TransactionSignatures, uint64(i)) 491 } 492 493 // For each siacoin input in the transaction that we added, provide a 494 // signature. 495 tb.wallet.mu.Lock() 496 defer tb.wallet.mu.Unlock() 497 for _, inputIndex := range tb.siacoinInputs { 498 input := tb.transaction.SiacoinInputs[inputIndex] 499 key := tb.wallet.keys[input.UnlockConditions.UnlockHash()] 500 newSigIndices, err := addSignatures(&tb.transaction, coveredFields, input.UnlockConditions, crypto.Hash(input.ParentID), key) 501 if err != nil { 502 return nil, err 503 } 504 tb.transactionSignatures = append(tb.transactionSignatures, newSigIndices...) 505 tb.signed = true // Signed is set to true after one successful signature to indicate that future signings can cause issues. 506 } 507 for _, inputIndex := range tb.siafundInputs { 508 input := tb.transaction.SiafundInputs[inputIndex] 509 key := tb.wallet.keys[input.UnlockConditions.UnlockHash()] 510 newSigIndices, err := addSignatures(&tb.transaction, coveredFields, input.UnlockConditions, crypto.Hash(input.ParentID), key) 511 if err != nil { 512 return nil, err 513 } 514 tb.transactionSignatures = append(tb.transactionSignatures, newSigIndices...) 515 tb.signed = true // Signed is set to true after one successful signature to indicate that future signings can cause issues. 516 } 517 518 // Get the transaction set and delete the transaction from the registry. 519 txnSet := append(tb.parents, tb.transaction) 520 return txnSet, nil 521 } 522 523 // ViewTransaction returns a transaction-in-progress along with all of its 524 // parents, specified by id. An error is returned if the id is invalid. Note 525 // that ids become invalid for a transaction after 'SignTransaction' has been 526 // called because the transaction gets deleted. 527 func (tb *transactionBuilder) View() (types.Transaction, []types.Transaction) { 528 return tb.transaction, tb.parents 529 } 530 531 // ViewAdded returns all of the siacoin inputs, siafund inputs, and parent 532 // transactions that have been automatically added by the builder. 533 func (tb *transactionBuilder) ViewAdded() (newParents, siacoinInputs, siafundInputs, transactionSignatures []int) { 534 return tb.newParents, tb.siacoinInputs, tb.siafundInputs, tb.transactionSignatures 535 } 536 537 // RegisterTransaction takes a transaction and its parents and returns a 538 // TransactionBuilder which can be used to expand the transaction. The most 539 // typical call is 'RegisterTransaction(types.Transaction{}, nil)', which 540 // registers a new transaction without parents. 541 func (w *Wallet) RegisterTransaction(t types.Transaction, parents []types.Transaction) modules.TransactionBuilder { 542 // Create a deep copy of the transaction and parents by encoding them. A 543 // deep copy ensures that there are no pointer or slice related errors - 544 // the builder will be working directly on the transaction, and the 545 // transaction may be in use elsewhere (in this case, the host is using the 546 // transaction. 547 pBytes := encoding.Marshal(parents) 548 var pCopy []types.Transaction 549 err := encoding.Unmarshal(pBytes, &pCopy) 550 if err != nil { 551 panic(err) 552 } 553 tBytes := encoding.Marshal(t) 554 var tCopy types.Transaction 555 err = encoding.Unmarshal(tBytes, &tCopy) 556 if err != nil { 557 panic(err) 558 } 559 return &transactionBuilder{ 560 parents: pCopy, 561 transaction: tCopy, 562 563 wallet: w, 564 } 565 } 566 567 // StartTransaction is a convenience function that calls 568 // RegisterTransaction(types.Transaction{}, nil). 569 func (w *Wallet) StartTransaction() modules.TransactionBuilder { 570 return w.RegisterTransaction(types.Transaction{}, nil) 571 }