mirror of
https://github.com/aljazceru/ark.git
synced 2025-12-18 04:34:19 +01:00
* scaffolding wallet * remove wallet db, add loader instead * wip * implement some wallet methods * signing and utxos * renaming * fee estimator * chain source options * config * application service * clark docker-compose * CLI refactor * v0 clark * v0.1 clark * fix SignTapscriptInput (btcwallet) * wallet.Broadcast, send via explora * fix ASP pubkey * Use lnd's btcwallet & Add rpc to get wallet staus * wip * unilateral exit * Fixes on watching for notifications and cli init * handle non-final BIP68 errors * Fixes * Fixes * Fix * a * fix onboard cosigners + revert tree validation * fix covenant e2e tests * fix covenantless e2e tests * fix container naming * fix lint error * update REAME.md * Add env var for wallet password --------- Co-authored-by: altafan <18440657+altafan@users.noreply.github.com>
225 lines
5.0 KiB
Go
225 lines
5.0 KiB
Go
package covenantless
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/ark-network/ark-cli/utils"
|
|
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
|
|
"github.com/ark-network/ark/common"
|
|
"github.com/urfave/cli/v2"
|
|
)
|
|
|
|
func (c *clArkBitcoinCLI) Send(ctx *cli.Context) error {
|
|
if !ctx.IsSet("receivers") && !ctx.IsSet("to") && !ctx.IsSet("amount") {
|
|
return fmt.Errorf("missing destination, either use --to and --amount to send or --receivers to send to many")
|
|
}
|
|
receivers := ctx.String("receivers")
|
|
to := ctx.String("to")
|
|
amount := ctx.Uint64("amount")
|
|
|
|
var receiversJSON []receiver
|
|
if len(receivers) > 0 {
|
|
if err := json.Unmarshal([]byte(receivers), &receiversJSON); err != nil {
|
|
return fmt.Errorf("invalid receivers: %s", err)
|
|
}
|
|
} else {
|
|
receiversJSON = []receiver{
|
|
{
|
|
To: to,
|
|
Amount: amount,
|
|
},
|
|
}
|
|
}
|
|
|
|
if len(receiversJSON) <= 0 {
|
|
return fmt.Errorf("no receivers specified")
|
|
}
|
|
|
|
onchainReceivers := make([]receiver, 0)
|
|
offchainReceivers := make([]receiver, 0)
|
|
|
|
for _, receiver := range receiversJSON {
|
|
if receiver.isOnchain() {
|
|
onchainReceivers = append(onchainReceivers, receiver)
|
|
} else {
|
|
offchainReceivers = append(offchainReceivers, receiver)
|
|
}
|
|
}
|
|
|
|
explorer := utils.NewExplorer(ctx)
|
|
|
|
if len(onchainReceivers) > 0 {
|
|
pset, err := sendOnchain(ctx, onchainReceivers)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
txid, err := explorer.Broadcast(pset)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return utils.PrintJSON(map[string]interface{}{
|
|
"txid": txid,
|
|
})
|
|
}
|
|
|
|
if len(offchainReceivers) > 0 {
|
|
if err := sendOffchain(ctx, offchainReceivers); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func sendOffchain(ctx *cli.Context, receivers []receiver) error {
|
|
withExpiryCoinselect := ctx.Bool("enable-expiry-coinselect")
|
|
|
|
offchainAddr, _, _, err := getAddress(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, _, aspPubKey, err := common.DecodeAddress(offchainAddr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
receiversOutput := make([]*arkv1.Output, 0)
|
|
sumOfReceivers := uint64(0)
|
|
|
|
for _, receiver := range receivers {
|
|
_, _, aspKey, err := common.DecodeAddress(receiver.To)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid receiver address: %s", err)
|
|
}
|
|
|
|
if !bytes.Equal(
|
|
aspPubKey.SerializeCompressed(), aspKey.SerializeCompressed(),
|
|
) {
|
|
return fmt.Errorf("invalid receiver address '%s': must be associated with the connected service provider", receiver.To)
|
|
}
|
|
|
|
if receiver.Amount < dust {
|
|
return fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount, dust)
|
|
}
|
|
|
|
receiversOutput = append(receiversOutput, &arkv1.Output{
|
|
Address: receiver.To,
|
|
Amount: uint64(receiver.Amount),
|
|
})
|
|
sumOfReceivers += receiver.Amount
|
|
}
|
|
client, close, err := getClientFromState(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer close()
|
|
|
|
explorer := utils.NewExplorer(ctx)
|
|
|
|
vtxos, err := getVtxos(ctx, explorer, client, offchainAddr, withExpiryCoinselect)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
selectedCoins, changeAmount, err := coinSelect(vtxos, sumOfReceivers, withExpiryCoinselect)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if changeAmount > 0 {
|
|
changeReceiver := &arkv1.Output{
|
|
Address: offchainAddr,
|
|
Amount: changeAmount,
|
|
}
|
|
receiversOutput = append(receiversOutput, changeReceiver)
|
|
}
|
|
|
|
inputs := make([]*arkv1.Input, 0, len(selectedCoins))
|
|
|
|
for _, coin := range selectedCoins {
|
|
inputs = append(inputs, &arkv1.Input{
|
|
Txid: coin.txid,
|
|
Vout: coin.vout,
|
|
})
|
|
}
|
|
|
|
secKey, err := utils.PrivateKeyFromPassword(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
registerResponse, err := client.RegisterPayment(
|
|
ctx.Context, &arkv1.RegisterPaymentRequest{Inputs: inputs},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = client.ClaimPayment(ctx.Context, &arkv1.ClaimPaymentRequest{
|
|
Id: registerResponse.GetId(),
|
|
Outputs: receiversOutput,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
poolTxID, err := handleRoundStream(
|
|
ctx, client, registerResponse.GetId(),
|
|
selectedCoins, secKey, receiversOutput,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return utils.PrintJSON(map[string]interface{}{
|
|
"pool_txid": poolTxID,
|
|
})
|
|
}
|
|
|
|
func coinSelect(vtxos []vtxo, amount uint64, sortByExpirationTime bool) ([]vtxo, uint64, error) {
|
|
selected := make([]vtxo, 0)
|
|
notSelected := make([]vtxo, 0)
|
|
selectedAmount := uint64(0)
|
|
|
|
if sortByExpirationTime {
|
|
// sort vtxos by expiration (older first)
|
|
sort.SliceStable(vtxos, func(i, j int) bool {
|
|
if vtxos[i].expireAt == nil || vtxos[j].expireAt == nil {
|
|
return false
|
|
}
|
|
|
|
return vtxos[i].expireAt.Before(*vtxos[j].expireAt)
|
|
})
|
|
}
|
|
|
|
for _, vtxo := range vtxos {
|
|
if selectedAmount >= amount {
|
|
notSelected = append(notSelected, vtxo)
|
|
break
|
|
}
|
|
|
|
selected = append(selected, vtxo)
|
|
selectedAmount += vtxo.amount
|
|
}
|
|
|
|
if selectedAmount < amount {
|
|
return nil, 0, fmt.Errorf("not enough funds to cover amount%d", amount)
|
|
}
|
|
|
|
change := selectedAmount - amount
|
|
|
|
if change < dust {
|
|
if len(notSelected) > 0 {
|
|
selected = append(selected, notSelected[0])
|
|
change += notSelected[0].amount
|
|
}
|
|
}
|
|
|
|
return selected, change, nil
|
|
}
|