mirror of
https://github.com/aljazceru/ark.git
synced 2025-12-18 04:34:19 +01:00
* [domain] add reverse boarding inputs in Payment struct * [tx-builder] support reverse boarding script * [wallet] add GetTransaction * [api-spec][application] add reverse boarding support in covenantless * [config] add reverse boarding config * [api-spec] add ReverseBoardingAddress RPC * [domain][application] support empty forfeits txs in EndFinalization events * [tx-builder] optional connector output in round tx * [btc-embedded] fix getTx and taproot finalizer * whitelist ReverseBoardingAddress RPC * [test] add reverse boarding integration test * [client] support reverse boarding * [sdk] support reverse boarding * [e2e] add sleep time after faucet * [test] run using bitcoin-core RPC * [tx-builder] fix GetSweepInput * [application][tx-builder] support reverse onboarding in covenant * [cli] support reverse onboarding in covenant CLI * [test] rework integration tests * [sdk] remove onchain wallet, replace by onboarding address * remove old onboarding protocols * [sdk] Fix RegisterPayment * [e2e] add more funds to covenant ASP * [e2e] add sleeping time * several fixes * descriptor boarding * remove boarding delay from info * [sdk] implement descriptor boarding * go mod tidy * fixes and revert error msgs * move descriptor pkg to common * add replace in go.mod * [sdk] fix unit tests * rename DescriptorInput --> BoardingInput * genrest in SDK * remove boarding input from domain * remove all "reverse boarding" * rename "onboarding" ==> "boarding" * remove outdate payment unit test * use tmpfs docker volument for compose testing files * several fixes
210 lines
4.8 KiB
Go
210 lines
4.8 KiB
Go
package covenantless
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
|
|
"github.com/ark-network/ark/client/utils"
|
|
"github.com/ark-network/ark/common"
|
|
"github.com/btcsuite/btcd/btcutil/psbt"
|
|
"github.com/urfave/cli/v2"
|
|
)
|
|
|
|
func (c *clArkBitcoinCLI) SendAsync(ctx *cli.Context) error {
|
|
receiverAddr := ctx.String("to")
|
|
amount := ctx.Uint64("amount")
|
|
withExpiryCoinselect := ctx.Bool("enable-expiry-coinselect")
|
|
|
|
if amount < dust {
|
|
return fmt.Errorf("invalid amount (%d), must be greater than dust %d", amount, dust)
|
|
}
|
|
|
|
if receiverAddr == "" {
|
|
return fmt.Errorf("receiver address is required")
|
|
}
|
|
isOnchain, _, _, err := decodeReceiverAddress(receiverAddr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if isOnchain {
|
|
txid, err := sendOnchain(ctx, []receiver{{receiverAddr, amount}})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return utils.PrintJSON(map[string]interface{}{
|
|
"txid": txid,
|
|
})
|
|
}
|
|
|
|
offchainAddr, _, _, err := getAddress(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, _, aspPubKey, err := common.DecodeAddress(offchainAddr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, _, aspKey, err := common.DecodeAddress(receiverAddr)
|
|
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", receiverAddr)
|
|
}
|
|
|
|
receiversOutput := make([]*arkv1.Output, 0)
|
|
sumOfReceivers := uint64(0)
|
|
receiversOutput = append(receiversOutput, &arkv1.Output{
|
|
Address: receiverAddr,
|
|
Amount: amount,
|
|
})
|
|
sumOfReceivers += 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{
|
|
Input: &arkv1.Input_VtxoInput{
|
|
VtxoInput: &arkv1.VtxoInput{
|
|
Txid: coin.txid,
|
|
Vout: coin.vout,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
resp, err := client.CreatePayment(
|
|
ctx.Context, &arkv1.CreatePaymentRequest{
|
|
Inputs: inputs,
|
|
Outputs: receiversOutput,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO verify the redeem tx signature
|
|
fmt.Println("payment created")
|
|
fmt.Println("signing redeem and forfeit txs...")
|
|
|
|
seckey, err := utils.PrivateKeyFromPassword(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
signedUnconditionalForfeitTxs := make([]string, 0, len(resp.UsignedUnconditionalForfeitTxs))
|
|
for _, tx := range resp.UsignedUnconditionalForfeitTxs {
|
|
forfeitPtx, err := psbt.NewFromRawBytes(strings.NewReader(tx), true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := signPsbt(ctx, forfeitPtx, explorer, seckey); err != nil {
|
|
return err
|
|
}
|
|
|
|
signedForfeitTx, err := forfeitPtx.B64Encode()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
signedUnconditionalForfeitTxs = append(signedUnconditionalForfeitTxs, signedForfeitTx)
|
|
}
|
|
|
|
redeemPtx, err := psbt.NewFromRawBytes(strings.NewReader(resp.SignedRedeemTx), true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := signPsbt(ctx, redeemPtx, explorer, seckey); err != nil {
|
|
return err
|
|
}
|
|
|
|
signedRedeem, err := redeemPtx.B64Encode()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err = client.CompletePayment(ctx.Context, &arkv1.CompletePaymentRequest{
|
|
SignedRedeemTx: signedRedeem,
|
|
SignedUnconditionalForfeitTxs: signedUnconditionalForfeitTxs,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Println("payment completed")
|
|
return nil
|
|
}
|
|
|
|
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 > 0 && change < dust {
|
|
if len(notSelected) > 0 {
|
|
selected = append(selected, notSelected[0])
|
|
change += notSelected[0].amount
|
|
}
|
|
}
|
|
|
|
return selected, change, nil
|
|
}
|