Skip to content

Commit

Permalink
add prepaid bonds
Browse files Browse the repository at this point in the history
  • Loading branch information
buck54321 committed Apr 5, 2024
1 parent b0a5fe9 commit fef9800
Show file tree
Hide file tree
Showing 31 changed files with 574 additions and 53 deletions.
133 changes: 129 additions & 4 deletions client/core/bond.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"decred.org/dcrdex/dex"
"decred.org/dcrdex/dex/keygen"
"decred.org/dcrdex/dex/msgjson"
"decred.org/dcrdex/server/account"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/decred/dcrd/hdkeychain/v3"
)
Expand Down Expand Up @@ -807,20 +808,21 @@ func (c *Core) postBond(dc *dexConnection, bond *asset.Bond) (*msgjson.PostBondR
return nil, codedError(registerErr, err)
}

dc.acct.authMtx.Lock()
dc.updateReputation(postBondRes.Reputation, postBondRes.Tier, nil, nil)
dc.acct.authMtx.Unlock()

// Check the response signature.
err = dc.acct.checkSig(postBondRes.Serialize(), postBondRes.Sig)
if err != nil {
c.log.Warnf("postbond: DEX signature validation error: %v", err)
}

if !bytes.Equal(postBondRes.BondID, bondCoin) {
return nil, fmt.Errorf("server reported bond coin ID %v, expected %v", coinIDString(assetID, postBondRes.BondID),
bondCoinStr)
}

dc.acct.authMtx.Lock()
dc.updateReputation(postBondRes.Reputation, postBondRes.Tier, nil, nil)
dc.acct.authMtx.Unlock()

return postBondRes, nil
}

Expand Down Expand Up @@ -926,6 +928,129 @@ func (c *Core) monitorBondConfs(dc *dexConnection, bond *asset.Bond, reqConfs ui
})
}

// RedeemPrepaidBond redeems a pre-paid bond for a dcrdex host server.
func (c *Core) RedeemPrepaidBond(appPW []byte, code []byte, host string, certI any) (tier uint64, err error) {
// Make sure the app has been initialized.
if !c.IsInitialized() {
return 0, fmt.Errorf("app not initialized")
}

// Check the app password.
crypter, err := c.encryptionKey(appPW)
if err != nil {
return 0, codedError(passwordErr, err)
}
defer crypter.Close()

var success, acctExists bool

c.connMtx.RLock()
dc, found := c.conns[host]
c.connMtx.RUnlock()
if found {
acctExists = !dc.acct.isViewOnly()
if acctExists {
if dc.acct.locked() { // require authDEX first to reconcile any existing bond statuses
return 0, newError(acctKeyErr, "acct locked %s (login first)", host)
}
}
} else {
// New DEX connection.
cert, err := parseCert(host, certI, c.net)
if err != nil {
return 0, newError(fileReadErr, "failed to read certificate file from %s: %v", cert, err)
}
dc, err = c.connectDEX(&db.AccountInfo{
Host: host,
Cert: cert,
// bond maintenance options set below.
})
if err != nil {
return 0, codedError(connectionErr, err)
}

// Close the connection to the dex server if the registration fails.
defer func() {
if !success {
dc.connMaster.Disconnect()
}
}()
}

if !acctExists { // new dex connection or pre-existing view-only connection
_, err := c.discoverAccount(dc, crypter)
if err != nil {
return 0, err
}
}

pkBytes := dc.acct.pubKey()
if len(pkBytes) == 0 {
return 0, fmt.Errorf("account keys not decrypted")
}

// Do a postbond request with the raw bytes of the unsigned tx, the bond
// script, and our account pubkey.
postBond := &msgjson.PostBond{
AcctPubKey: pkBytes,
AssetID: account.PrepaidBondID,
// Version: 0,
CoinID: code,
}
postBondRes := new(msgjson.PostBondResult)
if err = dc.signAndRequest(postBond, msgjson.PostBondRoute, postBondRes, DefaultResponseTimeout); err != nil {
return 0, codedError(registerErr, err)
}

// Check the response signature.
err = dc.acct.checkSig(postBondRes.Serialize(), postBondRes.Sig)
if err != nil {
c.log.Warnf("postbond: DEX signature validation error: %v", err)
}

lockTime := postBondRes.Expiry + dc.config().BondExpiry

dbBond := &db.Bond{
// Version: 0,
AssetID: account.PrepaidBondID,
CoinID: code,
LockTime: lockTime,
Strength: postBondRes.Strength,
Confirmed: true,
}

dc.acct.authMtx.Lock()
dc.updateReputation(postBondRes.Reputation, postBondRes.Tier, nil, nil)
dc.acct.bonds = append(dc.acct.bonds, dbBond)
dc.acct.authMtx.Unlock()

if acctExists {
err = c.db.AddBond(dc.acct.host, dbBond)
if err != nil {
return 0, fmt.Errorf("failed to store pre-paid bond for dex %s: %w", host, err)
}
} else {
dc.acct.keyMtx.RLock()
ai := &db.AccountInfo{
Host: dc.acct.host,
Cert: dc.acct.cert,
DEXPubKey: dc.acct.dexPubKey,
EncKeyV2: dc.acct.encKey,
Bonds: []*db.Bond{dbBond},
}
dc.acct.keyMtx.RUnlock()
err = c.dbCreateOrUpdateAccount(dc, ai)
if err != nil {
return 0, fmt.Errorf("failed to store pre-paid account for dex %s: %w", host, err)
}
}

c.updateBondReserves()

success = true
return uint64(postBondRes.Strength), nil
}

func deriveBondKey(bondXPriv *hdkeychain.ExtendedKey, assetID, bondIndex uint32) (*secp256k1.PrivateKey, error) {
kids := []uint32{
assetID + hdkeychain.HardenedKeyStart,
Expand Down
44 changes: 43 additions & 1 deletion client/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -4004,7 +4004,7 @@ func (c *Core) GetDEXConfig(dexAddr string, certI any) (*Exchange, error) {
// activity without setting up account keys or communicating account identity
// with the DEX. DiscoverAccount, Post Bond or Register (deprecated) may be used
// to set up a trading account for this DEX if required.
func (c *Core) AddDEX(dexAddr string, certI any) error {
func (c *Core) AddDEX(appPW []byte, dexAddr string, certI any) error {
if !c.IsInitialized() { // TODO: Allow adding view-only DEX without init.
return fmt.Errorf("cannot register DEX because app has not been initialized")
}
Expand Down Expand Up @@ -4068,6 +4068,23 @@ func (c *Core) AddDEX(dexAddr string, certI any) error {
c.conns[dc.acct.host] = dc
c.connMtx.Unlock()

// If a password was provided, try discoverAccount, but OK if we don't find
// it.
if len(appPW) > 0 {
crypter, err := c.encryptionKey(appPW)
if err != nil {
return codedError(passwordErr, err)
}
defer crypter.Close()

paid, err := c.discoverAccount(dc, crypter)
if err != nil {
c.log.Errorf("discoverAccount error during AddDEX: %v", err)
} else if paid {
c.upgradeConnection(dc)
}
}

return nil
}

Expand Down Expand Up @@ -7038,6 +7055,10 @@ func (c *Core) findBondKeyIdx(pkhEqualFn func(bondKey *secp256k1.PrivateKey) boo
// findBond will attempt to find an unknown bond and add it to the live bonds
// slice and db for refunding later. Returns the bond strength if no error.
func (c *Core) findBond(dc *dexConnection, bond *msgjson.Bond) (strength, bondAssetID uint32) {
if bond.AssetID == account.PrepaidBondID {
c.insertPrepaidBond(dc, bond)
return bond.Strength, bond.AssetID
}
symb := dex.BipIDSymbol(bond.AssetID)
bondIDStr := coinIDString(bond.AssetID, bond.CoinID)
c.log.Warnf("Unknown bond reported by server: %v (%s)", bondIDStr, symb)
Expand Down Expand Up @@ -7103,6 +7124,27 @@ func (c *Core) findBond(dc *dexConnection, bond *msgjson.Bond) (strength, bondAs
return strength, bondDetails.AssetID
}

func (c *Core) insertPrepaidBond(dc *dexConnection, bond *msgjson.Bond) {
lockTime := bond.Expiry + dc.config().BondExpiry
dbBond := &db.Bond{
Version: bond.Version,
AssetID: bond.AssetID,
CoinID: bond.CoinID,
LockTime: lockTime,
Strength: bond.Strength,
Confirmed: true,
}

err := c.db.AddBond(dc.acct.host, dbBond)
if err != nil {
c.log.Errorf("Failed to store pre-paid bond dex %v: %w", dc.acct.host, err)
}

dc.acct.authMtx.Lock()
dc.acct.bonds = append(dc.acct.bonds, dbBond)
dc.acct.authMtx.Unlock()
}

func (dc *dexConnection) maxScore() uint32 {
if maxScore := dc.config().MaxScore; maxScore > 0 {
return maxScore
Expand Down
3 changes: 3 additions & 0 deletions client/core/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -1116,6 +1116,9 @@ func token(id []byte) string {
// coinIDString converts a coin ID to a human-readable string. If an error is
// encountered the value starting with "<invalid coin>:" prefix is returned.
func coinIDString(assetID uint32, coinID []byte) string {
if assetID == account.PrepaidBondID {
return "prepaid-bond:" + hex.EncodeToString(coinID)
}
coinStr, err := asset.DecodeCoinID(assetID, coinID)
if err != nil {
// Logging error here with fmt.Printf is better than dropping it. It's not
Expand Down
41 changes: 39 additions & 2 deletions client/webserver/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,15 @@ func (s *WebServer) apiAddDEX(w http.ResponseWriter, r *http.Request) {
if !readPost(w, r, form) {
return
}
cert := []byte(form.Cert)
err := s.core.AddDEX(form.Addr, cert)
defer form.AppPW.Clear()
appPW, err := s.resolvePass(form.AppPW, r)
if err != nil {
s.writeAPIError(w, fmt.Errorf("password error: %w", err))
return
}
cert := []byte(form.Cert)

if err = s.core.AddDEX(appPW, form.Addr, cert); err != nil {
s.writeAPIError(w, err)
return
}
Expand Down Expand Up @@ -469,6 +475,37 @@ func (s *WebServer) apiUpdateBondOptions(w http.ResponseWriter, r *http.Request)
writeJSON(w, simpleAck(), s.indent)
}

func (s *WebServer) apiRedeemPrepaidBond(w http.ResponseWriter, r *http.Request) {
var req struct {
Host string `json:"host"`
Code dex.Bytes `json:"code"`
AppPW encode.PassBytes `json:"appPW"`
Cert string `json:"cert"`
}
defer req.AppPW.Clear()
if !readPost(w, r, &req) {
return
}
appPW, err := s.resolvePass(req.AppPW, r)
if err != nil {
s.writeAPIError(w, fmt.Errorf("password error: %w", err))
return
}
tier, err := s.core.RedeemPrepaidBond(appPW, req.Code, req.Host, req.Cert)
if err != nil {
s.writeAPIError(w, err)
return
}
resp := &struct {
OK bool `json:"ok"`
Tier uint64 `json:"tier"`
}{
OK: true,
Tier: tier,
}
writeJSON(w, resp, s.indent)
}

// apiNewWallet is the handler for the '/newwallet' API request.
func (s *WebServer) apiNewWallet(w http.ResponseWriter, r *http.Request) {
form := new(newWalletForm)
Expand Down
5 changes: 4 additions & 1 deletion client/webserver/live_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,7 @@ func (c *TCore) GetDEXConfig(host string, certI any) (*core.Exchange, error) {
return tExchanges[firstDEX], nil
}

func (c *TCore) AddDEX(dexAddr string, certI any) error {
func (c *TCore) AddDEX(appPW []byte, dexAddr string, certI any) error {
randomDelay()
if initErrors {
return fmt.Errorf("forced init error")
Expand Down Expand Up @@ -677,6 +677,9 @@ func (c *TCore) PostBond(form *core.PostBondForm) (*core.PostBondResult, error)
ReqConfirms: uint16(ba.Confs),
}, nil
}
func (c *TCore) RedeemPrepaidBond(appPW []byte, code []byte, host string, certI any) (tier uint64, err error) {
return 1, nil
}
func (c *TCore) UpdateBondOptions(form *core.BondOptionsForm) error {
return nil
}
Expand Down
21 changes: 20 additions & 1 deletion client/webserver/site/src/html/forms.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@
<h5>[[[current_bonding_asset]]]</h5>
<div data-tmpl="chooseDifferentAsset" class="fs16 underline grey pointer hoverbg">[[[choose a different asset]]]</div>
</div>
<div data-tmpl="assetSelection">
<div data-tmpl="assetSelection" class="mb03">
<h5 class="text-center">[[[Select your bond asset]]]</h5>
<div data-tmpl="whatsABond" class="flex-center fs18 hoverbg pointer underline py-2 mb-2">[[[what_s_a_bond]]]</div>
<table class="cell-border mb-3">
Expand Down Expand Up @@ -368,7 +368,26 @@
</div>
</div>
</div>
</div>
<div data-tmpl="otherOptions">
<hr class="dashed mb-3 mt-0">
<button type="button" data-tmpl="usePrepaidBond" class="small">Use a pre-paid bond</button>
</div>
<div data-tmpl="prepaidBonds" class="d-hide">
<div class="py-2 fs14 grey">
<span class="hoverbg pointer" data-tmpl="ppbGoBack"><span class="ico-arrowback fs12 me-1"></span> go back</span>
</div>
<h3>Redeem Pre-paid Bond</h3>
<label for="prepaidBondCode">Code</label>
<input type="text" data-tmpl="prepaidBondCode" autocomplete="off">
<div data-tmpl="prepaidBondPWBox">
<label for="ppbAppPW">[[[Password]]]</label>
<input type="password" data-tmpl="prepaidBondPW" autocomplete="off">
</div>
<div data-tmpl="prepaidBondErr" class="pt-2 px-2 text-danger d-hide"></div>
<div class="text-left mt-2">
<button data-tmpl="submitPrepaidBond" type="button" class="go">[[[Submit]]]</button>
</div>
</div>
{{end}}

Expand Down
1 change: 0 additions & 1 deletion client/webserver/site/src/html/register.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
<form class="d-hide" id="walletWait">
{{template "waitingForWalletForm"}}
</form>

</div>
</div>
{{template "bottom"}}
Expand Down

0 comments on commit fef9800

Please sign in to comment.