Skip to content
This repository has been archived by the owner on Mar 28, 2023. It is now read-only.

Commit

Permalink
Merge pull request #180 from cpacia/master
Browse files Browse the repository at this point in the history
Dispute Resolution
  • Loading branch information
cpacia authored Dec 21, 2016
2 parents c689136 + bc20350 commit 7034447
Show file tree
Hide file tree
Showing 50 changed files with 3,873 additions and 386 deletions.
2 changes: 1 addition & 1 deletion Godeps/Godeps.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions api/endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ func post(i *jsonAPIHandler, path string, w http.ResponseWriter, r *http.Request
i.POSTRefund(w, r)
case "/wallet/resyncblockchain", "/wallet/resyncblockchain/":
i.POSTResyncBlockchain(w, r)
case "/ob/opendispute", "/ob/opendispute/":
i.POSTOpenDispute(w, r)
case "/ob/closedispute", "/ob/closedispute/":
i.POSTCloseDispute(w, r)
case "/ob/releasefunds", "/ob/releasefunds/":
i.POSTReleaseFunds(w, r)
case "/ob/shutdown", "/ob/shutdown/":
i.POSTShutdown(w, r)
default:
Expand Down Expand Up @@ -108,6 +114,10 @@ func get(i *jsonAPIHandler, path string, w http.ResponseWriter, r *http.Request)
default:
ErrorResponse(w, http.StatusNotFound, "Not Found")
}
if strings.Contains(path, "/ob/case") {
i.GETCase(w, r)
return
}
}

func patch(i *jsonAPIHandler, path string, w http.ResponseWriter, r *http.Request) {
Expand Down
151 changes: 151 additions & 0 deletions api/jsonapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/OpenBazaar/spvwallet"
btc "github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/base58"
"github.com/golang/protobuf/ptypes/timestamp"
lockfile "github.com/ipfs/go-ipfs/repo/fsrepo/lock"
routing "github.com/ipfs/go-ipfs/routing/dht"
"github.com/jbenet/go-multiaddr"
Expand Down Expand Up @@ -1420,3 +1421,153 @@ func (i *jsonAPIHandler) POSTOrderComplete(w http.ResponseWriter, r *http.Reques
fmt.Fprint(w, `{}`)
return
}

func (i *jsonAPIHandler) POSTOpenDispute(w http.ResponseWriter, r *http.Request) {
type dispute struct {
OrderID string `json:"orderId"`
Claim string `json:"claim"`
}
decoder := json.NewDecoder(r.Body)
var d dispute
err := decoder.Decode(&d)
if err != nil {
ErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
var isSale bool
var contract *pb.RicardianContract
var state pb.OrderState
var records []*spvwallet.TransactionRecord
contract, state, _, records, _, err = i.node.Datastore.Purchases().GetByOrderId(d.OrderID)
if err != nil {
contract, state, _, records, _, err = i.node.Datastore.Sales().GetByOrderId(d.OrderID)
if err != nil {
ErrorResponse(w, http.StatusNotFound, "Order not found")
return
}
isSale = true
}
if contract.BuyerOrder.Payment.Method != pb.Order_Payment_MODERATED {
ErrorResponse(w, http.StatusBadRequest, "Only moderated orders can be disputed")
return
}

if isSale && (state != pb.OrderState_FUNDED && state != pb.OrderState_FULFILLED) {
ErrorResponse(w, http.StatusBadRequest, "Order must be either funded or fulfilled to start a dispute")
return
}
if !isSale && (state != pb.OrderState_CONFIRMED && state != pb.OrderState_FUNDED && state != pb.OrderState_FULFILLED) {
ErrorResponse(w, http.StatusBadRequest, "Order must be either confirmed, funded, or fulfilled to start a dispute")
return
}

err = i.node.OpenDispute(d.OrderID, contract, records, d.Claim)
if err != nil {
ErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
fmt.Fprint(w, `{}`)
return
}

func (i *jsonAPIHandler) POSTCloseDispute(w http.ResponseWriter, r *http.Request) {
type dispute struct {
OrderID string `json:"orderId"`
Resolution string `json:"resolution"`
BuyerPercentage float32 `json:"buyerPercentage"`
VendorPercentage float32 `json:"vendorPercentage"`
}
decoder := json.NewDecoder(r.Body)
var d dispute
err := decoder.Decode(&d)
if err != nil {
ErrorResponse(w, http.StatusBadRequest, err.Error())
return
}

err = i.node.CloseDispute(d.OrderID, d.BuyerPercentage, d.VendorPercentage, d.Resolution)
if err != nil && err == core.ErrCaseNotFound {
ErrorResponse(w, http.StatusNotFound, err.Error())
return
} else if err != nil {
ErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
fmt.Fprint(w, `{}`)
return
}

func (i *jsonAPIHandler) GETCase(w http.ResponseWriter, r *http.Request) {
_, orderId := path.Split(r.URL.Path)
buyerContract, vendorContract, buyerErrors, vendorErrors, state, read, date, buyerOpened, claim, resolution, err := i.node.Datastore.Cases().GetCaseMetadata(orderId)
if err != nil {
ErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}

resp := new(pb.CaseRespApi)
ts := new(timestamp.Timestamp)
ts.Seconds = int64(date.Unix())
ts.Nanos = 0
resp.BuyerContract = buyerContract
resp.VendorContract = vendorContract
resp.BuyerOpened = buyerOpened
resp.BuyerContractValidationErrors = buyerErrors
resp.VendorContractValidationErrors = vendorErrors
resp.Read = read
resp.State = state
resp.Claim = claim
resp.Resolution = resolution

m := jsonpb.Marshaler{
EnumsAsInts: false,
EmitDefaults: true,
Indent: " ",
OrigName: false,
}
out, err := m.MarshalToString(resp)
if err != nil {
ErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}

i.node.Datastore.Cases().MarkAsRead(orderId)
fmt.Fprint(w, out)
}

func (i *jsonAPIHandler) POSTReleaseFunds(w http.ResponseWriter, r *http.Request) {
type release struct {
OrderID string `json:"orderId"`
}
decoder := json.NewDecoder(r.Body)
var rel release
err := decoder.Decode(&rel)
if err != nil {
ErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
var contract *pb.RicardianContract
var state pb.OrderState
var records []*spvwallet.TransactionRecord
contract, state, _, records, _, err = i.node.Datastore.Purchases().GetByOrderId(rel.OrderID)
if err != nil {
contract, state, _, records, _, err = i.node.Datastore.Sales().GetByOrderId(rel.OrderID)
if err != nil {
ErrorResponse(w, http.StatusNotFound, "Order not found")
return
}
}

if state != pb.OrderState_DECIDED {
ErrorResponse(w, http.StatusBadRequest, "Order must be in DECIDED state to release funds")
return
}

err = i.node.ReleaseFunds(contract, records)
if err != nil {
ErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
fmt.Fprint(w, `{}`)
return
}
42 changes: 42 additions & 0 deletions api/notifications/notifications.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ type completionWrapper struct {
CompletionNotification `json:"orderCompletion"`
}

type disputeOpenWrapper struct {
DisputeOpenNotification `json:"disputeOpen"`
}

type disputeUpdateWrapper struct {
DisputeUpdateNotification `json:"disputeUpdate"`
}

type disputeCloseWrapper struct {
DisputeCloseNotification `json:"disputeClose"`
}

type OrderNotification struct {
Title string `json:"title"`
BuyerGuid string `json:"buyerGuid"`
Expand Down Expand Up @@ -70,6 +82,18 @@ type CompletionNotification struct {
OrderId string `json:"orderId"`
}

type DisputeOpenNotification struct {
OrderId string `json:"orderId"`
}

type DisputeUpdateNotification struct {
OrderId string `json:"orderId"`
}

type DisputeCloseNotification struct {
OrderId string `json:"orderId"`
}

type FollowNotification struct {
Follow string `json:"follow"`
}
Expand Down Expand Up @@ -123,6 +147,24 @@ func Serialize(i interface{}) []byte {
CompletionNotification: i.(CompletionNotification),
},
}
case DisputeOpenNotification:
n = notificationWrapper{
disputeOpenWrapper{
DisputeOpenNotification: i.(DisputeOpenNotification),
},
}
case DisputeUpdateNotification:
n = notificationWrapper{
disputeUpdateWrapper{
DisputeUpdateNotification: i.(DisputeUpdateNotification),
},
}
case DisputeCloseNotification:
n = notificationWrapper{
disputeCloseWrapper{
DisputeCloseNotification: i.(DisputeCloseNotification),
},
}
case FollowNotification:
n = notificationWrapper{
i.(FollowNotification),
Expand Down
19 changes: 19 additions & 0 deletions bitcoin/bitcoind/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,14 @@ func (w *BitcoindWallet) CurrentAddress(purpose spvwallet.KeyPurpose) btc.Addres
return addr
}

func (w *BitcoindWallet) HasKey(addr btc.Address) bool {
_, err := w.rpcClient.DumpPrivKey(addr)
if err != nil {
return false
}
return true
}

func (w *BitcoindWallet) Balance() (confirmed, unconfirmed int64) {
u, _ := w.rpcClient.GetUnconfirmedBalance(account)
c, _ := w.rpcClient.GetBalance(account)
Expand Down Expand Up @@ -180,6 +188,17 @@ func (w *BitcoindWallet) GetFeePerByte(feeLevel spvwallet.FeeLevel) uint64 {
return uint64(fee)
}

func (w *BitcoindWallet) EstimateFee(ins []spvwallet.TransactionInput, outs []spvwallet.TransactionOutput, feePerByte uint64) uint64 {
tx := new(wire.MsgTx)
for _, out := range outs {
output := wire.NewTxOut(out.Value, out.ScriptPubKey)
tx.TxOut = append(tx.TxOut, output)
}
estimatedSize := spvwallet.EstimateSerializeSize(len(ins), tx.TxOut, false)
fee := estimatedSize * int(feePerByte)
return uint64(fee)
}

func (w *BitcoindWallet) CreateMultisigSignature(ins []spvwallet.TransactionInput, outs []spvwallet.TransactionOutput, key *hd.ExtendedKey, redeemScript []byte, feePerByte uint64) ([]spvwallet.Signature, error) {
var sigs []spvwallet.Signature
tx := new(wire.MsgTx)
Expand Down
13 changes: 11 additions & 2 deletions bitcoin/listeners/transaction_listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func (l *TransactionListener) OnTransactionReceived(cb spvwallet.TransactionCall
isForSale := true
contract, state, funded, records, err := l.db.Sales().GetByPaymentAddress(addrs[0])
if err != nil {
contract, _, funded, records, err = l.db.Purchases().GetByPaymentAddress(addrs[0])
contract, state, funded, records, err = l.db.Purchases().GetByPaymentAddress(addrs[0])
if err != nil {
continue
}
Expand All @@ -79,9 +79,11 @@ func (l *TransactionListener) OnTransactionReceived(cb spvwallet.TransactionCall
continue
}

var fundsReleased bool
for _, r := range records {
if r.Txid == outpointHash.String() && r.Index == input.OutpointIndex {
r.Spent = true
fundsReleased = true
}
}

Expand All @@ -94,10 +96,16 @@ func (l *TransactionListener) OnTransactionReceived(cb spvwallet.TransactionCall
records = append(records, record)
if isForSale {
l.db.Sales().UpdateFunding(orderId, funded, records)
// This is a dispute payout. We should set the order state.
if state == pb.OrderState_DECIDED && len(records) > 0 && fundsReleased {
l.db.Sales().Put(orderId, *contract, pb.OrderState_RESOLVED, false)
}
} else {
l.db.Purchases().UpdateFunding(orderId, funded, records)
if state == pb.OrderState_CONFIRMED {
l.db.Purchases().Put(orderId, *contract, pb.OrderState_FUNDED, false)
} else if state == pb.OrderState_DECIDED && len(records) > 0 && fundsReleased {
l.db.Purchases().Put(orderId, *contract, pb.OrderState_RESOLVED, false)
}
}
}
Expand Down Expand Up @@ -143,6 +151,7 @@ func (l *TransactionListener) processSalePayment(txid []byte, output spvwallet.T
l.broadcast <- n
}
}

record := &spvwallet.TransactionRecord{
Txid: chainHash.String(),
Index: output.Index,
Expand Down Expand Up @@ -184,9 +193,9 @@ func (l *TransactionListener) processPurchasePayment(txid []byte, output spvwall
orderId,
uint64(funding),
})
log.Notice(state)
l.broadcast <- n
}

record := &spvwallet.TransactionRecord{
Txid: chainHash.String(),
Index: output.Index,
Expand Down
6 changes: 6 additions & 0 deletions bitcoin/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ type BitcoinWallet interface {
// Get the current address for the given purpose
CurrentAddress(purpose spvwallet.KeyPurpose) btc.Address

// Returns if the wallet has the key for the given address
HasKey(addr btc.Address) bool

// Get the confirmed and unconfirmed balances
Balance() (confirmed, unconfirmed int64)

Expand All @@ -39,6 +42,9 @@ type BitcoinWallet interface {
// Send bitcoins to an external wallet
Spend(amount int64, addr btc.Address, feeLevel spvwallet.FeeLevel) error

// Calculates the estimated size of the transaction and returns the total fee for the given feePerByte
EstimateFee(ins []spvwallet.TransactionInput, outs []spvwallet.TransactionOutput, feePerByte uint64) uint64

// Build and broadcast a transaction that sweeps all coins from a 1 of 2 multisig to an internal address
SweepMultisig(utxos []spvwallet.Utxo, key *hd.ExtendedKey, reddemScript []byte, feeLevel spvwallet.FeeLevel) error

Expand Down
2 changes: 1 addition & 1 deletion core/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"encoding/hex"
"encoding/json"
"errors"
"gx/ipfs/QmT6n4mspWYEya864BhCUJEgyxiRfmiSY9ruQwTUNpRKaM/protobuf/proto"
"gx/ipfs/QmYf7ng2hG5XBtJA3tN34DQ2GUN5HNksEw1rLDkmr6vGku/go-multihash"
"io/ioutil"
"os"
Expand All @@ -20,6 +19,7 @@ import (
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcutil"
hd "github.com/btcsuite/btcutil/hdkeychain"
"github.com/golang/protobuf/proto"
"github.com/golang/protobuf/ptypes/timestamp"
)

Expand Down
Loading

0 comments on commit 7034447

Please sign in to comment.