Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/asset/importall/importall.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
_ "decred.org/dcrdex/client/asset/dgb" // register dgb asset
_ "decred.org/dcrdex/client/asset/doge" // register doge asset
_ "decred.org/dcrdex/client/asset/firo" // register firo asset
_ "decred.org/dcrdex/client/asset/lbc" // register lbc asset
_ "decred.org/dcrdex/client/asset/ltc" // register ltc asset
_ "decred.org/dcrdex/client/asset/zec" // register zec asset
// nixed
Expand Down
1 change: 1 addition & 0 deletions client/asset/importall/importall_xmr.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
_ "decred.org/dcrdex/client/asset/dgb" // register dgb asset
_ "decred.org/dcrdex/client/asset/doge" // register doge asset
_ "decred.org/dcrdex/client/asset/firo" // register firo asset
_ "decred.org/dcrdex/client/asset/lbc" // register lbc asset
_ "decred.org/dcrdex/client/asset/ltc" // register ltc asset
_ "decred.org/dcrdex/client/asset/xmr" // register xmr asset
_ "decred.org/dcrdex/client/asset/zec" // register zec asset
Expand Down
182 changes: 182 additions & 0 deletions client/asset/lbc/lbc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// This code is available on the terms of the project LICENSE.md file,
// also available online at https://blueoakcouncil.org/license/1.0.0.

package lbc

import (
"context"
"fmt"
"math"
"strconv"

"decred.org/dcrdex/client/asset"
"decred.org/dcrdex/client/asset/btc"
"decred.org/dcrdex/dex"
dexbtc "decred.org/dcrdex/dex/networks/btc"
dexlbc "decred.org/dcrdex/dex/networks/lbc"
"github.com/btcsuite/btcd/chaincfg"
)

const (
version = 0
// BipID is the BIP-0044 / SLIP-0044 coin type for LBRY Credits.
BipID = 140

// minNetworkVersion is intentionally low: local/dev lbcd builds report a
// small version.Numeric(). Operators of released binaries will still pass.
minNetworkVersion = 0
walletTypeRPC = "lbcwalletRPC"
)

var (
configOpts = append(btc.RPCConfigOpts("LBRY Credits", "9244"), []*asset.ConfigOption{
{
Key: "fallbackfee",
DisplayName: "Fallback fee rate",
Description: "LBC's fallback fee rate. Units: LBC/kB",
DefaultValue: strconv.FormatFloat(dexlbc.DefaultFee*1000/1e8, 'f', -1, 64),
},
{
Key: "feeratelimit",
DisplayName: "Highest acceptable fee rate",
Description: "This is the highest network fee rate you are willing to " +
"pay on swap transactions. If feeratelimit is lower than a market's " +
"maxfeerate, you will not be able to trade on that market with this " +
"wallet. Units: LBC/kB",
DefaultValue: strconv.FormatFloat(dexlbc.DefaultFeeRateLimit*1000/1e8, 'f', -1, 64),
},
{
Key: "txsplit",
DisplayName: "Pre-split funding inputs",
Description: "When placing an order, create a \"split\" transaction to fund the order without locking more of the wallet balance than " +
"necessary. Otherwise, excess funds may be reserved to fund the order until the first swap contract is broadcast " +
"during match settlement, or the order is canceled. This is an extra transaction for which network mining fees are paid. " +
"Used only for standing-type orders, e.g. limit orders without immediate time-in-force.",
IsBoolean: true,
DefaultValue: "true",
},
}...)
// WalletInfo defines some general information about an LBC wallet.
WalletInfo = &asset.WalletInfo{
Name: "LBRY Credits",
SupportedVersions: []uint32{version},
UnitInfo: dexlbc.UnitInfo,
AvailableWallets: []*asset.WalletDefinition{{
Type: walletTypeRPC,
Tab: "External",
Description: "Connect to lbcwallet (which must be connected to lbcd)",
DefaultConfigPath: dexbtc.SystemConfigPath("lbcwallet"),
ConfigOpts: configOpts,
}},
BlockchainClass: asset.BlockchainClassUTXO,
}
)

func init() {
asset.Register(BipID, &Driver{})
}

// Driver implements asset.Driver.
type Driver struct{}

// Open creates the LBC exchange wallet.
func (d *Driver) Open(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) (asset.Wallet, error) {
return NewWallet(cfg, logger, network)
}

// DecodeCoinID creates a human-readable representation of a coin ID for LBC.
func (d *Driver) DecodeCoinID(coinID []byte) (string, error) {
return (&btc.Driver{}).DecodeCoinID(coinID)
}

// Info returns basic information about the wallet and asset.
func (d *Driver) Info() *asset.WalletInfo {
return WalletInfo
}

// MinLotSize calculates the minimum lot size for a given fee rate.
func (d *Driver) MinLotSize(maxFeeRate uint64) uint64 {
return dexbtc.MinLotSize(maxFeeRate, false)
}

func toSatoshi(v float64) uint64 {
return uint64(math.Round(v * 1e8))
}

// NewWallet is the exported constructor by which the DEX will import the
// exchange wallet. Connect to lbcwallet's legacy JSON-RPC (default port 9244).
// lbcwallet must be connected to an lbcd node for chain RPCs (passthrough).
func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) (asset.Wallet, error) {
var params *chaincfg.Params
switch network {
case dex.Mainnet:
params = dexlbc.MainNetParams
case dex.Testnet:
params = dexlbc.TestNet3Params
case dex.Regtest:
params = dexlbc.RegressionNetParams
default:
return nil, fmt.Errorf("unknown network ID %v", network)
}

// Wallet RPC ports (lbcwallet), not node ports.
ports := dexbtc.NetPorts{
Mainnet: "9244",
Testnet: "19244",
Simnet: "29244",
}

// w is closed over by BalanceFunc / FeeEstimator (same pattern as ZCL).
var w *btc.ExchangeWalletFullNode
cloneCFG := &btc.BTCCloneCFG{
WalletCFG: cfg,
MinNetworkVersion: minNetworkVersion,
WalletInfo: WalletInfo,
Symbol: "lbc",
Logger: logger,
Network: network,
ChainParams: params,
Ports: ports,
DefaultFallbackFee: dexlbc.DefaultFee,
DefaultFeeRateLimit: dexlbc.DefaultFeeRateLimit,
// lbcwallet has no getwalletinfo / getbalances; use getbalance.
BalanceFunc: func(ctx context.Context, locked uint64) (*asset.Balance, error) {
var bal float64
// minconf=0 to include unconfirmed; account "" is default.
if err := w.CallRPC("getbalance", []any{"*", 0}, &bal); err != nil {
// Fallback: no-arg getbalance
if err2 := w.CallRPC("getbalance", nil, &bal); err2 != nil {
return nil, fmt.Errorf("getbalance: %v (fallback: %v)", err, err2)
}
}
return &asset.Balance{
Available: toSatoshi(bal) - locked,
Locked: locked,
Other: make(map[asset.BalanceCategory]asset.CustomBalance),
}, nil
},
// Non-segwit for lbcwallet RPC compatibility: getrawchangeaddress takes
// (account, addresstype); dcrdex would pass "bech32" as account. Legacy
// P2SH swap contracts still work on LBC mainnet (SegWit is optional).
// Follow-up: add AccountFirstChangeAddr support and enable Segwit.
Segwit: false,
InitTxSize: dexbtc.InitTxSize,
InitTxSizeBase: dexbtc.InitTxSizeBase,
OmitAddressType: true,
LegacySignTxRPC: true,
LegacyValidateAddressRPC: true,
SingularWallet: true,
UnlockSpends: true, // lbcwallet may not auto-unlock spent coins
BlockDeserializer: dexlbc.DeserializeBlock,
AssetID: BipID,
FeeEstimator: func(ctx context.Context, cl btc.RawRequester, confTarget uint64) (uint64, error) {
// Prefer estimatesmartfee if lbcd provides it via passthrough.
// Fall back to DefaultFee.
return dexlbc.DefaultFee, nil
},
}

var err error
w, err = btc.BTCCloneWallet(cloneCFG)
return w, err
}
42 changes: 42 additions & 0 deletions client/asset/lbc/regnet_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//go:build harness

package lbc

// Regnet tests expect the LBC test harness to be running.
//
// cd client/asset/lbc
// go test -v -count=1 -tags=harness -run TestWallet

import (
"testing"

"decred.org/dcrdex/client/asset/btc/livetest"
"decred.org/dcrdex/dex"
)

var (
tLotSize uint64 = 1e6
tLBC = &dex.Asset{
ID: BipID,
Symbol: "lbc",
Version: version,
MaxFeeRate: 100,
SwapConf: 1,
}
)

func TestWallet(t *testing.T) {
livetest.Run(t, &livetest.Config{
NewWallet: NewWallet,
LotSize: tLotSize,
Asset: tLBC,
FirstWallet: &livetest.WalletName{
Node: "alpha",
WalletType: walletTypeRPC,
},
SecondWallet: &livetest.WalletName{
Node: "beta",
WalletType: walletTypeRPC,
},
})
}
Binary file added client/webserver/site/src/img/coins/lbc.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions client/webserver/site/src/js/coinexplorers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ export const CoinExplorers: Record<number, Record<number, (cid: string) => strin
[Testnet]: (cid: string) => `https://testexplorer.firo.org/tx/${cid.split(':')[0]}`,
[Simnet]: (cid: string) => `https://explorer.firo.org/tx/${cid.split(':')[0]}`
},
140: { // lbc (LBRY Credits)
[Mainnet]: (cid: string) => `https://explorer.lbry.com/tx/${cid.split(':')[0]}`,
[Testnet]: (cid: string) => `https://explorer.lbry.com/tx/${cid.split(':')[0]}`,
[Simnet]: (cid: string) => `https://explorer.lbry.com/tx/${cid.split(':')[0]}`
},
145: { // bch
[Mainnet]: (cid: string) => `https://bch.loping.net/tx/${cid.split(':')[0]}`,
[Testnet]: (cid: string) => `https://tbch4.loping.net/tx/${cid.split(':')[0]}`,
Expand Down
1 change: 1 addition & 0 deletions client/webserver/site/src/js/doc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const BipIDs: Record<number, string> = {
128: 'xmr',
136: 'firo',
133: 'zec',
140: 'lbc', // LBRY Credits
966: 'polygon',
966001: 'usdc.polygon',
966002: 'weth.polygon',
Expand Down
122 changes: 122 additions & 0 deletions dex/networks/lbc/block.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// This code is available on the terms of the project LICENSE.md file,
// also available online at https://blueoakcouncil.org/license/1.0.0.

package lbc

import (
"bytes"
"fmt"
"io"
"time"

"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
)

// LBC block headers are 112 bytes: the usual Bitcoin fields plus a ClaimTrie
// hash (32 bytes) after MerkleRoot.
//
// Layout: Version | PrevBlock | MerkleRoot | ClaimTrie | Timestamp | Bits | Nonce
const (
claimTrieSize = 32
lbcBlockHeaderLen = 112
)

// DeserializeBlock decodes a serialized LBC block into a btcsuite MsgBlock.
// Transactions use standard Bitcoin wire encoding (including SegWit).
//
// NOTE: The returned Header does not retain ClaimTrie. Consequently
// Header.BlockHash() / MsgBlock.BlockHash() are NOT the LBC block hash.
// Callers that need the real hash should use node RPC (getblockhash, etc.).
// PrevBlock, MerkleRoot, Timestamp, Version, Bits, and Nonce are correct.
func DeserializeBlock(blk []byte) (*wire.MsgBlock, error) {
return DeserializeBlockBytes(blk)
}

// DeserializeBlockBytes is an alias used by some clone backends.
func DeserializeBlockBytes(blk []byte) (*wire.MsgBlock, error) {
r := bytes.NewReader(blk)

hdr, err := deserializeHeader(r)
if err != nil {
return nil, fmt.Errorf("failed to deserialize LBC block header: %w", err)
}

txnCount, err := wire.ReadVarInt(r, 0)
if err != nil {
return nil, fmt.Errorf("failed to parse transaction count: %w", err)
}

txns := make([]*wire.MsgTx, int(txnCount))
for i := range txns {
msgTx := &wire.MsgTx{}
if err = msgTx.Deserialize(r); err != nil {
return nil, fmt.Errorf("failed to deserialize transaction %d of %d: %w",
i+1, txnCount, err)
}
txns[i] = msgTx
}

return &wire.MsgBlock{
Header: *hdr,
Transactions: txns,
}, nil
}

func deserializeHeader(r io.Reader) (*wire.BlockHeader, error) {
var (
version int32
prevBlock, merkle, claimTr chainhash.Hash
timestamp uint32
bits, nonce uint32
)

if err := readElements(r,
&version,
&prevBlock,
&merkle,
&claimTr, // discarded for wire.BlockHeader
&timestamp,
&bits,
&nonce,
); err != nil {
return nil, err
}
_ = claimTr

return &wire.BlockHeader{
Version: version,
PrevBlock: prevBlock,
MerkleRoot: merkle,
Timestamp: time.Unix(int64(timestamp), 0),
Bits: bits,
Nonce: nonce,
}, nil
}

// readElements reads successive binary little-endian fields from r.
func readElements(r io.Reader, elements ...interface{}) error {
for _, el := range elements {
switch e := el.(type) {
case *int32:
var b [4]byte
if _, err := io.ReadFull(r, b[:]); err != nil {
return err
}
*e = int32(uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24)
case *uint32:
var b [4]byte
if _, err := io.ReadFull(r, b[:]); err != nil {
return err
}
*e = uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
case *chainhash.Hash:
if _, err := io.ReadFull(r, e[:]); err != nil {
return err
}
default:
return fmt.Errorf("unsupported element type %T", el)
}
}
return nil
}
Loading