Skip to content

Commit

Permalink
Fix transfer & Update deps & Hotfix bug (#65)
Browse files Browse the repository at this point in the history
* Add configurable dust amount (defaults to `450`)

* Fix transfer rpc

* Update deps and hotfix panic bug
  • Loading branch information
altafan authored Dec 29, 2023
1 parent 2726d99 commit 4e10dc2
Show file tree
Hide file tree
Showing 9 changed files with 154 additions and 102 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ run: clean
export OCEAN_STATS_INTERVAL=120; \
export OCEAN_ELECTRUM_URL=tcp://localhost:50001; \
export OCEAN_UTXO_EXPIRY_DURATION_IN_SECONDS=60; \
export OCEAN_DB_TYPE=badger; \
go run ./cmd/oceand

test: fmt
Expand Down
2 changes: 2 additions & 0 deletions cmd/oceand/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ var (
dbPort = config.GetInt(config.DbPortKey)
dbName = config.GetString(config.DbNameKey)
migrationSourceURL = config.GetString(config.DbMigrationPath)
dustAmount = uint64(config.GetInt(config.DustAmountKey))
)

func main() {
Expand Down Expand Up @@ -87,6 +88,7 @@ func main() {
RootPath: rootPath,
Network: network,
UtxoExpiryDuration: utxoExpiryDuration * time.Second,
DustAmount: dustAmount,
RepoManagerType: dbType,
BlockchainScannerType: bcScannerType,
RepoManagerConfig: repoManagerConfig,
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ require (
github.com/stretchr/testify v1.8.1
github.com/timshannon/badgerhold/v4 v4.0.2
github.com/vulpemventures/go-bip39 v1.0.2
github.com/vulpemventures/go-elements v0.4.5
github.com/vulpemventures/go-elements v0.5.1
github.com/vulpemventures/neutrino-elements v0.1.3
golang.org/x/crypto v0.1.0
golang.org/x/net v0.7.0
Expand Down Expand Up @@ -83,7 +83,7 @@ require (
github.com/stretchr/objx v0.5.0 // indirect
github.com/subosito/gotenv v1.3.0 // indirect
github.com/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941 // indirect
github.com/vulpemventures/go-secp256k1-zkp v1.1.5 // indirect
github.com/vulpemventures/go-secp256k1-zkp v1.1.6 // indirect
go.opencensus.io v0.24.0 // indirect
go.uber.org/atomic v1.10.0 // indirect
golang.org/x/sys v0.5.0 // indirect
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1203,8 +1203,11 @@ github.com/vulpemventures/go-bip39 v1.0.2 h1:+BgKOVo04okKf1wA4Fhv8ccvql+qFyzVUdV
github.com/vulpemventures/go-bip39 v1.0.2/go.mod h1:mjFmuv9D6BtANI6iscMmbHhmBOwjMCFfny3mxHnuDrk=
github.com/vulpemventures/go-elements v0.4.5 h1:NlD68xcou9eYDIZiGTo7xpFuJV0IzcF4VQeix9cSdFU=
github.com/vulpemventures/go-elements v0.4.5/go.mod h1:S7wy2QnrC2ElCZOscYMU2PYnnXRmuBQYfLMQGMVBqMg=
github.com/vulpemventures/go-elements v0.5.1 h1:F83n7dScOnAnpyH9VgWLdC68Qcva+2EWVzzNuW2UKak=
github.com/vulpemventures/go-elements v0.5.1/go.mod h1:aBGuWXHaiAIUIcwqCdtEh2iQ3kJjKwHU9ywvhlcRSeU=
github.com/vulpemventures/go-secp256k1-zkp v1.1.5 h1:oG1kO8ibVQ1wOvYcnFyuI+2YqnEZluXdRwkOPJlHBQM=
github.com/vulpemventures/go-secp256k1-zkp v1.1.5/go.mod h1:zo7CpgkuPgoe7fAV+inyxsI9IhGmcoFgyD8nqZaPSOM=
github.com/vulpemventures/go-secp256k1-zkp v1.1.6/go.mod h1:zo7CpgkuPgoe7fAV+inyxsI9IhGmcoFgyD8nqZaPSOM=
github.com/vulpemventures/neutrino-elements v0.1.3 h1:rzg6G1oL2fWodOZ716SOs71JuqttPCjjmig7TrlHrxM=
github.com/vulpemventures/neutrino-elements v0.1.3/go.mod h1:pSl1A515bPEw9PxnyVwxyTpXH5GZNMMBacOCs7Ioyds=
github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
Expand Down
6 changes: 5 additions & 1 deletion internal/app-config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type AppConfig struct {
RootPath string
Network *network.Network
UtxoExpiryDuration time.Duration
DustAmount uint64

RepoManagerType string
BlockchainScannerType string
Expand All @@ -59,6 +60,9 @@ func (c *AppConfig) Validate() error {
if c.UtxoExpiryDuration == 0 {
return fmt.Errorf("missing utxo expiry duration")
}
if c.DustAmount == 0 {
return fmt.Errorf("missing dust amount threshold")
}
if len(c.RepoManagerType) == 0 {
return fmt.Errorf("missing repo manager type")
}
Expand Down Expand Up @@ -252,7 +256,7 @@ func (c *AppConfig) transactionService() *application.TransactionService {
rm, _ := c.repoManager()
bcs, _ := c.bcScanner()
c.txSvc = application.NewTransactionService(
rm, bcs, c.Network, c.UtxoExpiryDuration,
rm, bcs, c.Network, c.UtxoExpiryDuration, c.DustAmount,
)
return c.txSvc
}
Expand Down
4 changes: 4 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ const (
DbNameKey = "DB_NAME"
// DbMigrationPath is the path to migration files
DbMigrationPath = "DB_MIGRATION_PATH"
// DustAmountKey is the key to customize the dust amount threshold
DustAmountKey = "DUST_AMOUNT"

// DbLocation is the folder inside the datadir containing db files.
DbLocation = "db"
Expand Down Expand Up @@ -105,6 +107,7 @@ var (
defaultUtxoExpiryDuration = 360 // 6 minutes (3 blocks)
defaultEsploraUrl = "https://blockstream.info/liquid/api"
defaultElectrumUrl = "ssl://blockstream.info:995"
defaultDustAmount = uint64(450)

supportedNetworks = map[string]*network.Network{
network.Liquid.Name: &network.Liquid,
Expand Down Expand Up @@ -152,6 +155,7 @@ func init() {
vip.SetDefault(DbNameKey, "oceand-db")
vip.SetDefault(DbMigrationPath, "file://internal/infrastructure/storage/db/postgres/migration")
vip.SetDefault(ElectrumUrlKey, defaultElectrumUrl)
vip.SetDefault(DustAmountKey, defaultDustAmount)

if err := validate(); err != nil {
log.Fatalf("invalid config: %s", err)
Expand Down
225 changes: 129 additions & 96 deletions internal/core/application/transaction_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,21 @@ type TransactionService struct {
bcScanner ports.BlockchainScanner
network *network.Network
utxoExpiryDuration time.Duration
dustAmount uint64

log func(format string, a ...interface{})
}

func NewTransactionService(
repoManager ports.RepoManager, bcScanner ports.BlockchainScanner,
net *network.Network, utxoExpiryDuration time.Duration,
net *network.Network, utxoExpiryDuration time.Duration, dustAmount uint64,
) *TransactionService {
logFn := func(format string, a ...interface{}) {
format = fmt.Sprintf("transaction service: %s", format)
log.Debugf(format, a...)
}
svc := &TransactionService{
repoManager, bcScanner, net, utxoExpiryDuration, logFn,
repoManager, bcScanner, net, utxoExpiryDuration, dustAmount, logFn,
}
svc.registerHandlerForUtxoEvents()
svc.registerHandlerForWalletEvents()
Expand Down Expand Up @@ -328,6 +329,15 @@ func (ts *TransactionService) Transfer(
ctx context.Context, accountName string, outputs Outputs,
millisatsPerByte uint64,
) (string, error) {
// Ensure lbtc outs are not dust.
for _, out := range outputs {
if out.Asset == ts.network.AssetID {
if out.Amount < ts.dustAmount {
return "", fmt.Errorf("lbtc output amount must not be dust")
}
}
}

w, err := ts.getWallet(ctx)
if err != nil {
return "", err
Expand Down Expand Up @@ -361,14 +371,20 @@ func (ts *TransactionService) Transfer(
changeByAsset := make(map[string]uint64)
selectedUtxos := make([]*domain.Utxo, 0)
lbtc := ts.network.AssetID
dust := uint64(0)
for targetAsset, targetAmount := range outputs.totalAmountByAsset() {
utxos, change, err := DefaultCoinSelector.SelectUtxos(utxos, targetAmount, targetAsset)
if err != nil {
return "", err
}
selectedUtxos = append(selectedUtxos, utxos...)
if change > 0 {
changeByAsset[targetAsset] = change
// If the lbtc change is dust, it is added as fee amount.
if targetAsset == lbtc && change < ts.dustAmount {
dust = change
} else {
changeByAsset[targetAsset] = change
}
}
}

Expand Down Expand Up @@ -422,117 +438,134 @@ func (ts *TransactionService) Transfer(
feeAmount := wallet.EstimateFees(
inputs, append(outs, changeOutputs...), millisatsPerByte,
)

// If feeAmount is lower than the lbtc change, it's enough to deduct it
// from the change amount.
if feeAmount < changeByAsset[lbtc] {
for i, out := range changeOutputs {
if out.Asset == lbtc {
changeOutputs[i].Amount -= feeAmount
break
}
}
}
// If feeAmount is exactly the lbtc change, it's enough to remove the
// change output.
if feeAmount == changeByAsset[lbtc] {
var outIndex int
for i, out := range changeOutputs {
if out.Asset == lbtc {
outIndex = i
break
}
}
changeOutputs = append(
changeOutputs[:outIndex], changeOutputs[outIndex+1:]...,
)
}
// If feeAmount is greater than the lbtc change, another coin-selection round
// is required only in case the user is not trasferring the whole balance.
// In that case the fee amount is deducted from the lbtc output with biggest.
if feeAmount > changeByAsset[lbtc] {
if changeByAsset[lbtc] == 0 {
outIndex := 0
for i, out := range outputs {
if dust >= feeAmount {
// If the dust amount covers the fee amount we are done as the dust
// just pays for the tx fees.
feeAmount = dust
} else {
// If lbtc change covers the fee amount, we subtract the latter from the
// former. The remaining lbtc change can be become fees if it end up being
// dust.
if feeAmount <= changeByAsset[lbtc] {
var outIndex int
for i, out := range changeOutputs {
if out.Asset == lbtc {
if out.Amount > outputs[outIndex].Amount {
outIndex = i
}
outIndex = i
changeOutputs[i].Amount -= feeAmount
break
}
}
outs[outIndex] = wallet.Output{
Asset: outs[outIndex].Asset,
Amount: outs[outIndex].Amount - feeAmount,
Script: outs[outIndex].Script,
BlindingKey: outs[outIndex].BlindingKey,
BlinderIndex: outs[outIndex].BlinderIndex,
}
} else {
targetAsset := lbtc
targetAmount := wallet.DummyFeeAmount
if feeAmount > targetAmount {
targetAmount = roundUpAmount(feeAmount)
}

// Coin-selection must be done over remaining utxos.
remainingUtxos := getRemainingUtxos(utxos, selectedUtxos)
selectedUtxos, change, err := DefaultCoinSelector.SelectUtxos(
remainingUtxos, targetAmount, targetAsset,
)
if err != nil {
return "", err
changeAmount := changeOutputs[outIndex].Amount
if changeAmount < ts.dustAmount {
changeOutputs = append(
changeOutputs[:outIndex], changeOutputs[outIndex+1:]...,
)
feeAmount += changeAmount
}
}

for _, u := range selectedUtxos {
input := wallet.Input{
TxID: u.TxID,
TxIndex: u.VOut,
Value: u.Value,
Asset: u.Asset,
Script: u.Script,
ValueBlinder: u.ValueBlinder,
AssetBlinder: u.AssetBlinder,
ValueCommitment: u.ValueCommitment,
AssetCommitment: u.AssetCommitment,
Nonce: u.Nonce,
// If feeAmount is greater than the lbtc change, another coin-selection
// round is required, but only in case the user is not trasferring the
// whole balance.
if feeAmount > changeByAsset[lbtc] {
if changeByAsset[lbtc] == 0 {
// If there's no dust it means no change was actually produced during
// the first coin selection. In this case, the fee amount is subtracted
// from the output of the same asset with the biggest amount.
if dust == 0 {
outIndex := 0
outAmount := uint64(0)
for i, out := range outputs {
if out.Asset == lbtc {
if out.Amount >= outAmount {
outIndex = i
outAmount = out.Amount
}
}
}
if outs[outIndex].Amount-feeAmount >= ts.dustAmount {
outs[outIndex] = wallet.Output{
Asset: outs[outIndex].Asset,
Amount: outs[outIndex].Amount - feeAmount,
Script: outs[outIndex].Script,
BlindingKey: outs[outIndex].BlindingKey,
BlinderIndex: outs[outIndex].BlinderIndex,
}
} else {
// Otherwise the target output becomes fees.
feeAmount = outs[outIndex].Amount
outs = append(
outs[:outIndex], outs[outIndex+1:]...,
)
}
}
} else {
targetAsset := lbtc
targetAmount := changeByAsset[lbtc] - feeAmount

// Coin-selection must be done over remaining utxos.
remainingUtxos := getRemainingUtxos(utxos, selectedUtxos)
selectedUtxos, _, err := DefaultCoinSelector.SelectUtxos(
remainingUtxos, targetAmount, targetAsset,
)
if err != nil {
return "", err
}
inputs = append(inputs, input)
inputsByIndex[uint32(len(inputs))] = input
}

if change > 0 {
// For the eventual change amount, it might be necessary to add a lbtc
// change output to the list if it's still not in the list.
if _, ok := changeByAsset[targetAsset]; !ok {
addrInfo, err := walletRepo.DeriveNextInternalAddressesForAccount(
ctx, account.Namespace, 1,
)
if err != nil {
return "", err
for _, u := range selectedUtxos {
input := wallet.Input{
TxID: u.TxID,
TxIndex: u.VOut,
Value: u.Value,
Asset: u.Asset,
Script: u.Script,
ValueBlinder: u.ValueBlinder,
AssetBlinder: u.AssetBlinder,
ValueCommitment: u.ValueCommitment,
AssetCommitment: u.AssetCommitment,
Nonce: u.Nonce,
}
script, _ := hex.DecodeString(addrInfo[0].Script)
changeOutputs = append(changeOutputs, wallet.Output{
Amount: change,
Asset: targetAsset,
Script: script,
BlindingKey: addrInfo[0].BlindingKey,
})
inputs = append(inputs, input)
inputsByIndex[uint32(len(inputs))] = input
}

// Now that we have all inputs and outputs, estimate the real fee amount.
feeAmount = wallet.EstimateFees(
feeAmount := wallet.EstimateFees(
inputs, append(outs, changeOutputs...), millisatsPerByte,
)

// Update the change amount by adding the delta
// delta = targetAmount - feeAmount.
outAmount := uint64(0)
for _, out := range outs {
if out.Asset == lbtc {
outAmount += out.Amount
}
}
inAmount := uint64(0)
for _, in := range inputs {
if in.Asset == lbtc {
inAmount += in.Value
}
}

// The change is calculated as:
// total in amount - (total out amount + fee amount)
changeAmount := inAmount - outAmount - feeAmount
changeIndex := 0
for i, out := range changeOutputs {
if out.Asset == targetAsset {
// This way the delta is subtracted in case it's negative.
changeOutputs[i].Amount = uint64(int(out.Amount) + int(targetAmount) - int(feeAmount))
if out.Asset == lbtc {
changeIndex = i
break
}
}

// The change output is updated and eventually removed if became dust,
changeOutputs[changeIndex].Amount = changeAmount
if changeAmount < ts.dustAmount {
feeAmount += changeAmount
changeOutputs = append(
changeOutputs[:changeIndex], changeOutputs[changeIndex+1:]...,
)
}
}
}
}
Expand Down
Loading

0 comments on commit 4e10dc2

Please sign in to comment.