mirror of
https://github.com/aljazceru/ark.git
synced 2026-01-17 10:44:21 +01:00
New boarding protocol (#279)
* [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
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
|
||||
"github.com/ark-network/ark/client/flags"
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
@@ -21,18 +22,30 @@ func (*covenantLiquidCLI) Balance(ctx *cli.Context) error {
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
offchainAddr, onchainAddr, redemptionAddr, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
network, err := utils.GetNetwork(ctx)
|
||||
offchainAddr, boardingAddr, redemptionAddr, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// No need to check for error here becuase this function is called also by getAddress().
|
||||
// nolint:all
|
||||
unilateralExitDelay, _ := utils.GetUnilateralExitDelay(ctx)
|
||||
|
||||
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(3)
|
||||
|
||||
@@ -54,19 +67,19 @@ func (*covenantLiquidCLI) Balance(ctx *cli.Context) error {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
balance, err := explorer.GetBalance(onchainAddr, toElementsNetwork(network).AssetID)
|
||||
spendableBalance, lockedBalance, err := explorer.GetDelayedBalance(boardingAddr, int64(timeoutBoarding))
|
||||
if err != nil {
|
||||
chRes <- balanceRes{0, 0, nil, nil, err}
|
||||
return
|
||||
}
|
||||
chRes <- balanceRes{0, balance, nil, nil, nil}
|
||||
chRes <- balanceRes{0, spendableBalance, lockedBalance, nil, nil}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
|
||||
spendableBalance, lockedBalance, err := explorer.GetRedeemedVtxosBalance(
|
||||
spendableBalance, lockedBalance, err := explorer.GetDelayedBalance(
|
||||
redemptionAddr, unilateralExitDelay,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
148
client/covenant/claim.go
Normal file
148
client/covenant/claim.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package covenant
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
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/descriptor"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func (c *covenantLiquidCLI) Claim(ctx *cli.Context) error {
|
||||
client, cancel, err := getClientFromState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
offchainAddr, boardingAddr, _, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
|
||||
boardingUtxosFromExplorer, err := explorer.GetUtxos(boardingAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
boardingUtxos := make([]utils.Utxo, 0, len(boardingUtxosFromExplorer))
|
||||
for _, utxo := range boardingUtxosFromExplorer {
|
||||
u := utils.NewUtxo(utxo, uint(timeoutBoarding))
|
||||
if u.SpendableAt.Before(now) {
|
||||
continue // cannot claim if onchain spendable
|
||||
}
|
||||
|
||||
boardingUtxos = append(boardingUtxos, u)
|
||||
}
|
||||
|
||||
var pendingBalance uint64
|
||||
|
||||
for _, utxo := range boardingUtxos {
|
||||
pendingBalance += utxo.Amount
|
||||
}
|
||||
|
||||
if pendingBalance == 0 {
|
||||
return fmt.Errorf("no boarding utxos to claim")
|
||||
}
|
||||
|
||||
receiver := receiver{
|
||||
To: offchainAddr,
|
||||
Amount: pendingBalance,
|
||||
}
|
||||
|
||||
if len(ctx.String("password")) == 0 {
|
||||
if ok := askForConfirmation(
|
||||
fmt.Sprintf(
|
||||
"claim %d satoshis from %d boarding utxos",
|
||||
pendingBalance, len(boardingUtxos),
|
||||
),
|
||||
); !ok {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return selfTransferAllPendingPayments(
|
||||
ctx, client, boardingUtxos, receiver, boardingDescriptor,
|
||||
)
|
||||
}
|
||||
|
||||
func selfTransferAllPendingPayments(
|
||||
ctx *cli.Context,
|
||||
client arkv1.ArkServiceClient,
|
||||
boardingUtxos []utils.Utxo,
|
||||
myself receiver,
|
||||
desc string,
|
||||
) error {
|
||||
inputs := make([]*arkv1.Input, 0, len(boardingUtxos))
|
||||
|
||||
for _, outpoint := range boardingUtxos {
|
||||
inputs = append(inputs, &arkv1.Input{
|
||||
Input: &arkv1.Input_BoardingInput{
|
||||
BoardingInput: &arkv1.BoardingInput{
|
||||
Txid: outpoint.Txid,
|
||||
Vout: outpoint.Vout,
|
||||
Descriptor_: desc,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
receiversOutput := []*arkv1.Output{
|
||||
{
|
||||
Address: myself.To,
|
||||
Amount: myself.Amount,
|
||||
},
|
||||
}
|
||||
|
||||
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: []*arkv1.Output{{Address: myself.To, Amount: myself.Amount}},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
poolTxID, err := handleRoundStream(
|
||||
ctx, client, registerResponse.GetId(), make([]vtxo, 0),
|
||||
len(boardingUtxos) > 0, secKey, receiversOutput,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utils.PrintJSON(map[string]interface{}{
|
||||
"pool_txid": poolTxID,
|
||||
})
|
||||
}
|
||||
@@ -9,16 +9,13 @@ import (
|
||||
"github.com/ark-network/ark/client/interfaces"
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/vulpemventures/go-elements/address"
|
||||
"github.com/vulpemventures/go-elements/elementsutil"
|
||||
"github.com/vulpemventures/go-elements/network"
|
||||
"github.com/vulpemventures/go-elements/payment"
|
||||
"github.com/vulpemventures/go-elements/psetv2"
|
||||
"github.com/vulpemventures/go-elements/taproot"
|
||||
"github.com/vulpemventures/go-elements/transaction"
|
||||
)
|
||||
|
||||
const dust = 450
|
||||
@@ -29,19 +26,15 @@ func (c *covenantLiquidCLI) SendAsync(ctx *cli.Context) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func (c *covenantLiquidCLI) ClaimAsync(ctx *cli.Context) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func (c *covenantLiquidCLI) Receive(ctx *cli.Context) error {
|
||||
offchainAddr, onchainAddr, _, err := getAddress(ctx)
|
||||
offchainAddr, boardingAddr, _, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utils.PrintJSON(map[string]interface{}{
|
||||
"offchain_address": offchainAddr,
|
||||
"onchain_address": onchainAddr,
|
||||
"boarding_address": boardingAddr,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -131,14 +124,14 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
|
||||
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
|
||||
utxos, delayedUtxos, change, err := coinSelectOnchain(
|
||||
utxos, change, err := coinSelectOnchain(
|
||||
ctx, explorer, targetAmount, nil,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := addInputs(ctx, updater, utxos, delayedUtxos, &liquidNet); err != nil {
|
||||
if err := addInputs(ctx, updater, utxos); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -181,14 +174,14 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
|
||||
updater.Pset.Outputs = updater.Pset.Outputs[:len(updater.Pset.Outputs)-1]
|
||||
}
|
||||
// reselect the difference
|
||||
selected, delayedSelected, newChange, err := coinSelectOnchain(
|
||||
ctx, explorer, feeAmount-change, append(utxos, delayedUtxos...),
|
||||
selected, newChange, err := coinSelectOnchain(
|
||||
ctx, explorer, feeAmount-change, utxos,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := addInputs(ctx, updater, selected, delayedSelected, &liquidNet); err != nil {
|
||||
if err := addInputs(ctx, updater, selected); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -243,20 +236,37 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
|
||||
func coinSelectOnchain(
|
||||
ctx *cli.Context,
|
||||
explorer utils.Explorer, targetAmount uint64, exclude []utils.Utxo,
|
||||
) ([]utils.Utxo, []utils.Utxo, uint64, error) {
|
||||
_, onchainAddr, _, err := getAddress(ctx)
|
||||
) ([]utils.Utxo, uint64, error) {
|
||||
_, boardingAddr, redemptionAddr, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
fromExplorer, err := explorer.GetUtxos(onchainAddr)
|
||||
boardingUtxosFromExplorer, err := explorer.GetUtxos(boardingAddr)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
utxos := make([]utils.Utxo, 0)
|
||||
selectedAmount := uint64(0)
|
||||
for _, utxo := range fromExplorer {
|
||||
now := time.Now()
|
||||
|
||||
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
for _, utxo := range boardingUtxosFromExplorer {
|
||||
if selectedAmount >= targetAmount {
|
||||
break
|
||||
}
|
||||
@@ -267,207 +277,99 @@ func coinSelectOnchain(
|
||||
}
|
||||
}
|
||||
|
||||
utxos = append(utxos, utxo)
|
||||
selectedAmount += utxo.Amount
|
||||
utxo := utils.NewUtxo(utxo, uint(timeoutBoarding))
|
||||
|
||||
if utxo.SpendableAt.Before(now) {
|
||||
utxos = append(utxos, utxo)
|
||||
selectedAmount += utxo.Amount
|
||||
}
|
||||
}
|
||||
|
||||
if selectedAmount >= targetAmount {
|
||||
return utxos, nil, selectedAmount - targetAmount, nil
|
||||
return utxos, selectedAmount - targetAmount, nil
|
||||
}
|
||||
|
||||
userPubkey, err := utils.GetWalletPublicKey(ctx)
|
||||
redemptionUtxosFromExplorer, err := explorer.GetUtxos(redemptionAddr)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
aspPubkey, err := utils.GetAspPublicKey(ctx)
|
||||
vtxoExitDelay, err := utils.GetUnilateralExitDelay(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
unilateralExitDelay, err := utils.GetUnilateralExitDelay(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
vtxoTapKey, _, err := computeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, uint(unilateralExitDelay),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
net, err := utils.GetNetwork(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
liquidNet := toElementsNetwork(net)
|
||||
|
||||
pay, err := payment.FromTweakedKey(vtxoTapKey, &liquidNet, nil)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
addr, err := pay.TaprootAddress()
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
fromExplorer, err = explorer.GetUtxos(addr)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
delayedUtxos := make([]utils.Utxo, 0)
|
||||
for _, utxo := range fromExplorer {
|
||||
for _, utxo := range redemptionUtxosFromExplorer {
|
||||
if selectedAmount >= targetAmount {
|
||||
break
|
||||
}
|
||||
|
||||
availableAt := time.Unix(utxo.Status.Blocktime, 0).Add(
|
||||
time.Duration(unilateralExitDelay) * time.Second,
|
||||
)
|
||||
if availableAt.After(time.Now()) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, excluded := range exclude {
|
||||
if utxo.Txid == excluded.Txid && utxo.Vout == excluded.Vout {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
delayedUtxos = append(delayedUtxos, utxo)
|
||||
selectedAmount += utxo.Amount
|
||||
utxo := utils.NewUtxo(utxo, uint(vtxoExitDelay))
|
||||
|
||||
if utxo.SpendableAt.Before(now) {
|
||||
utxos = append(utxos, utxo)
|
||||
selectedAmount += utxo.Amount
|
||||
}
|
||||
}
|
||||
|
||||
if selectedAmount < targetAmount {
|
||||
return nil, nil, 0, fmt.Errorf(
|
||||
return nil, 0, fmt.Errorf(
|
||||
"not enough funds to cover amount %d", targetAmount,
|
||||
)
|
||||
}
|
||||
|
||||
return utxos, delayedUtxos, selectedAmount - targetAmount, nil
|
||||
return utxos, selectedAmount - targetAmount, nil
|
||||
}
|
||||
|
||||
func addInputs(
|
||||
ctx *cli.Context,
|
||||
updater *psetv2.Updater, utxos, delayedUtxos []utils.Utxo, net *network.Network,
|
||||
updater *psetv2.Updater,
|
||||
utxos []utils.Utxo,
|
||||
) error {
|
||||
_, onchainAddr, _, err := getAddress(ctx)
|
||||
userPubkey, err := utils.GetWalletPublicKey(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
changeScript, err := address.ToOutputScript(onchainAddr)
|
||||
aspPubkey, err := utils.GetAspPublicKey(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, utxo := range utxos {
|
||||
sequence, err := utxo.Sequence()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := updater.AddInputs([]psetv2.InputArgs{
|
||||
{
|
||||
Txid: utxo.Txid,
|
||||
TxIndex: utxo.Vout,
|
||||
Txid: utxo.Txid,
|
||||
TxIndex: utxo.Vout,
|
||||
Sequence: sequence,
|
||||
},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
assetID, err := elementsutil.AssetHashToBytes(utxo.Asset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
value, err := elementsutil.ValueToBytes(utxo.Amount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
witnessUtxo := transaction.TxOutput{
|
||||
Asset: assetID,
|
||||
Value: value,
|
||||
Script: changeScript,
|
||||
Nonce: []byte{0x00},
|
||||
}
|
||||
|
||||
if err := updater.AddInWitnessUtxo(
|
||||
len(updater.Pset.Inputs)-1, &witnessUtxo,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(delayedUtxos) > 0 {
|
||||
userPubkey, err := utils.GetWalletPublicKey(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aspPubkey, err := utils.GetAspPublicKey(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
unilateralExitDelay, err := utils.GetUnilateralExitDelay(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vtxoTapKey, leafProof, err := computeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, uint(unilateralExitDelay),
|
||||
_, leafProof, err := computeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, utxo.Delay,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pay, err := payment.FromTweakedKey(vtxoTapKey, net, nil)
|
||||
if err != nil {
|
||||
inputIndex := len(updater.Pset.Inputs) - 1
|
||||
|
||||
if err := updater.AddInTapLeafScript(inputIndex, psetv2.NewTapLeafScript(*leafProof, tree.UnspendableKey())); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
addr, err := pay.TaprootAddress()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
script, err := address.ToOutputScript(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, utxo := range delayedUtxos {
|
||||
if err := addVtxoInput(
|
||||
updater,
|
||||
psetv2.InputArgs{
|
||||
Txid: utxo.Txid,
|
||||
TxIndex: utxo.Vout,
|
||||
},
|
||||
uint(unilateralExitDelay),
|
||||
leafProof,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
assetID, err := elementsutil.AssetHashToBytes(utxo.Asset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
value, err := elementsutil.ValueToBytes(utxo.Amount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
witnessUtxo := transaction.NewTxOutput(assetID, value, script)
|
||||
|
||||
if err := updater.AddInWitnessUtxo(
|
||||
len(updater.Pset.Inputs)-1, witnessUtxo,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -503,32 +405,7 @@ func decodeReceiverAddress(addr string) (
|
||||
return true, outputScript, nil, nil
|
||||
}
|
||||
|
||||
func addVtxoInput(
|
||||
updater *psetv2.Updater, inputArgs psetv2.InputArgs, exitDelay uint,
|
||||
tapLeafProof *taproot.TapscriptElementsProof,
|
||||
) error {
|
||||
sequence, err := common.BIP68EncodeAsNumber(exitDelay)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
nextInputIndex := len(updater.Pset.Inputs)
|
||||
if err := updater.AddInputs([]psetv2.InputArgs{inputArgs}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updater.Pset.Inputs[nextInputIndex].Sequence = sequence
|
||||
|
||||
return updater.AddInTapLeafScript(
|
||||
nextInputIndex,
|
||||
psetv2.NewTapLeafScript(
|
||||
*tapLeafProof,
|
||||
tree.UnspendableKey(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func getAddress(ctx *cli.Context) (offchainAddr, onchainAddr, redemptionAddr string, err error) {
|
||||
func getAddress(ctx *cli.Context) (offchainAddr, boardingAddr, redemptionAddr string, err error) {
|
||||
userPubkey, err := utils.GetWalletPublicKey(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -544,6 +421,21 @@ func getAddress(ctx *cli.Context) (offchainAddr, onchainAddr, redemptionAddr str
|
||||
return
|
||||
}
|
||||
|
||||
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
arkNet, err := utils.GetNetwork(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -556,12 +448,6 @@ func getAddress(ctx *cli.Context) (offchainAddr, onchainAddr, redemptionAddr str
|
||||
|
||||
liquidNet := toElementsNetwork(arkNet)
|
||||
|
||||
p2wpkh := payment.FromPublicKey(userPubkey, &liquidNet, nil)
|
||||
liquidAddr, err := p2wpkh.WitnessPubKeyHash()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
vtxoTapKey, _, err := computeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, uint(unilateralExitDelay),
|
||||
)
|
||||
@@ -569,18 +455,34 @@ func getAddress(ctx *cli.Context) (offchainAddr, onchainAddr, redemptionAddr str
|
||||
return
|
||||
}
|
||||
|
||||
payment, err := payment.FromTweakedKey(vtxoTapKey, &liquidNet, nil)
|
||||
redemptionPay, err := payment.FromTweakedKey(vtxoTapKey, &liquidNet, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
redemptionAddr, err = payment.TaprootAddress()
|
||||
redemptionAddr, err = redemptionPay.TaprootAddress()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
boardingTapKey, _, err := computeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, uint(timeoutBoarding),
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
boardingPay, err := payment.FromTweakedKey(boardingTapKey, &liquidNet, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
boardingAddr, err = boardingPay.TaprootAddress()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
offchainAddr = arkAddr
|
||||
onchainAddr = liquidAddr
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -51,13 +51,16 @@ func getVtxos(
|
||||
if v.Swept {
|
||||
continue
|
||||
}
|
||||
vtxos = append(vtxos, vtxo{
|
||||
amount: v.Receiver.Amount,
|
||||
txid: v.Outpoint.Txid,
|
||||
vout: v.Outpoint.Vout,
|
||||
poolTxid: v.PoolTxid,
|
||||
expireAt: expireAt,
|
||||
})
|
||||
|
||||
if v.Outpoint.GetVtxoInput() != nil {
|
||||
vtxos = append(vtxos, vtxo{
|
||||
amount: v.Receiver.Amount,
|
||||
txid: v.Outpoint.GetVtxoInput().GetTxid(),
|
||||
vout: v.Outpoint.GetVtxoInput().GetVout(),
|
||||
poolTxid: v.PoolTxid,
|
||||
expireAt: expireAt,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if !computeExpiration {
|
||||
@@ -193,32 +196,10 @@ func toCongestionTree(treeFromProto *arkv1.Tree) (tree.CongestionTree, error) {
|
||||
return levels, nil
|
||||
}
|
||||
|
||||
// castCongestionTree converts a tree.CongestionTree to a repeated arkv1.TreeLevel
|
||||
func castCongestionTree(congestionTree tree.CongestionTree) *arkv1.Tree {
|
||||
levels := make([]*arkv1.TreeLevel, 0, len(congestionTree))
|
||||
for _, level := range congestionTree {
|
||||
levelProto := &arkv1.TreeLevel{
|
||||
Nodes: make([]*arkv1.Node, 0, len(level)),
|
||||
}
|
||||
|
||||
for _, node := range level {
|
||||
levelProto.Nodes = append(levelProto.Nodes, &arkv1.Node{
|
||||
Txid: node.Txid,
|
||||
Tx: node.Tx,
|
||||
ParentTxid: node.ParentTxid,
|
||||
})
|
||||
}
|
||||
|
||||
levels = append(levels, levelProto)
|
||||
}
|
||||
return &arkv1.Tree{
|
||||
Levels: levels,
|
||||
}
|
||||
}
|
||||
|
||||
func handleRoundStream(
|
||||
ctx *cli.Context, client arkv1.ArkServiceClient, paymentID string,
|
||||
vtxosToSign []vtxo, secKey *secp256k1.PrivateKey, receivers []*arkv1.Output,
|
||||
vtxosToSign []vtxo, mustSignRoundTx bool,
|
||||
secKey *secp256k1.PrivateKey, receivers []*arkv1.Output,
|
||||
) (poolTxID string, err error) {
|
||||
stream, err := client.GetEventStream(ctx.Context, &arkv1.GetEventStreamRequest{})
|
||||
if err != nil {
|
||||
@@ -254,8 +235,8 @@ func handleRoundStream(
|
||||
pingStop()
|
||||
fmt.Println("round finalization started")
|
||||
|
||||
poolTx := e.GetPoolTx()
|
||||
ptx, err := psetv2.NewPsetFromBase64(poolTx)
|
||||
roundTx := e.GetPoolTx()
|
||||
ptx, err := psetv2.NewPsetFromBase64(roundTx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -280,13 +261,13 @@ func handleRoundStream(
|
||||
if !isOnchainOnly(receivers) {
|
||||
// validate the congestion tree
|
||||
if err := tree.ValidateCongestionTree(
|
||||
congestionTree, poolTx, aspPubkey, int64(roundLifetime),
|
||||
congestionTree, roundTx, aspPubkey, int64(roundLifetime),
|
||||
); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
if err := common.ValidateConnectors(poolTx, connectors); err != nil {
|
||||
if err := common.ValidateConnectors(roundTx, connectors); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -379,78 +360,100 @@ func handleRoundStream(
|
||||
|
||||
fmt.Println("congestion tree validated")
|
||||
|
||||
forfeits := e.GetForfeitTxs()
|
||||
signedForfeits := make([]string, 0)
|
||||
|
||||
fmt.Print("signing forfeit txs... ")
|
||||
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
finalizePaymentRequest := &arkv1.FinalizePaymentRequest{}
|
||||
|
||||
connectorsTxids := make([]string, 0, len(connectors))
|
||||
for _, connector := range connectors {
|
||||
p, _ := psetv2.NewPsetFromBase64(connector)
|
||||
utx, _ := p.UnsignedTx()
|
||||
txid := utx.TxHash().String()
|
||||
if len(vtxosToSign) > 0 {
|
||||
forfeits := e.GetForfeitTxs()
|
||||
signedForfeits := make([]string, 0)
|
||||
|
||||
connectorsTxids = append(connectorsTxids, txid)
|
||||
fmt.Print("signing forfeit txs... ")
|
||||
|
||||
connectorsTxids := make([]string, 0, len(connectors))
|
||||
for _, connector := range connectors {
|
||||
p, _ := psetv2.NewPsetFromBase64(connector)
|
||||
utx, _ := p.UnsignedTx()
|
||||
txid := utx.TxHash().String()
|
||||
|
||||
connectorsTxids = append(connectorsTxids, txid)
|
||||
}
|
||||
|
||||
for _, forfeit := range forfeits {
|
||||
pset, err := psetv2.NewPsetFromBase64(forfeit)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, input := range pset.Inputs {
|
||||
inputTxid := chainhash.Hash(input.PreviousTxid).String()
|
||||
|
||||
for _, coin := range vtxosToSign {
|
||||
// check if it contains one of the input to sign
|
||||
if inputTxid == coin.txid {
|
||||
// verify that the connector is in the connectors list
|
||||
connectorTxid := chainhash.Hash(pset.Inputs[0].PreviousTxid).String()
|
||||
connectorFound := false
|
||||
for _, txid := range connectorsTxids {
|
||||
if txid == connectorTxid {
|
||||
connectorFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !connectorFound {
|
||||
return "", fmt.Errorf("connector txid %s not found in the connectors list", connectorTxid)
|
||||
}
|
||||
|
||||
if err := signPset(ctx, pset, explorer, secKey); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
signedPset, err := pset.ToBase64()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
signedForfeits = append(signedForfeits, signedPset)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if no forfeit txs have been signed, start pinging again and wait for the next round
|
||||
if len(signedForfeits) == 0 {
|
||||
fmt.Printf("\nno forfeit txs to sign, waiting for the next round...\n")
|
||||
pingStop = nil
|
||||
for pingStop == nil {
|
||||
pingStop = ping(ctx.Context, client, pingReq)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf("%d signed\n", len(signedForfeits))
|
||||
finalizePaymentRequest.SignedForfeitTxs = signedForfeits
|
||||
}
|
||||
|
||||
for _, forfeit := range forfeits {
|
||||
pset, err := psetv2.NewPsetFromBase64(forfeit)
|
||||
if mustSignRoundTx {
|
||||
ptx, err := psetv2.NewPsetFromBase64(roundTx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, input := range pset.Inputs {
|
||||
inputTxid := chainhash.Hash(input.PreviousTxid).String()
|
||||
|
||||
for _, coin := range vtxosToSign {
|
||||
// check if it contains one of the input to sign
|
||||
if inputTxid == coin.txid {
|
||||
// verify that the connector is in the connectors list
|
||||
connectorTxid := chainhash.Hash(pset.Inputs[0].PreviousTxid).String()
|
||||
connectorFound := false
|
||||
for _, txid := range connectorsTxids {
|
||||
if txid == connectorTxid {
|
||||
connectorFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !connectorFound {
|
||||
return "", fmt.Errorf("connector txid %s not found in the connectors list", connectorTxid)
|
||||
}
|
||||
|
||||
if err := signPset(ctx, pset, explorer, secKey); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
signedPset, err := pset.ToBase64()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
signedForfeits = append(signedForfeits, signedPset)
|
||||
}
|
||||
}
|
||||
if err := signPset(ctx, ptx, explorer, secKey); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
signedRoundTx, err := ptx.ToBase64()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fmt.Println("round tx signed")
|
||||
finalizePaymentRequest.SignedRoundTx = &signedRoundTx
|
||||
}
|
||||
|
||||
// if no forfeit txs have been signed, start pinging again and wait for the next round
|
||||
if len(signedForfeits) == 0 {
|
||||
fmt.Printf("\nno forfeit txs to sign, waiting for the next round...\n")
|
||||
pingStop = nil
|
||||
for pingStop == nil {
|
||||
pingStop = ping(ctx.Context, client, pingReq)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf("%d signed\n", len(signedForfeits))
|
||||
fmt.Print("finalizing payment... ")
|
||||
_, err = client.FinalizePayment(ctx.Context, &arkv1.FinalizePaymentRequest{
|
||||
SignedForfeitTxs: signedForfeits,
|
||||
})
|
||||
_, err = client.FinalizePayment(ctx.Context, finalizePaymentRequest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -104,6 +104,7 @@ func connectToAsp(ctx *cli.Context, net, url, explorer string) error {
|
||||
utils.ROUND_LIFETIME: strconv.Itoa(int(resp.GetRoundLifetime())),
|
||||
utils.UNILATERAL_EXIT_DELAY: strconv.Itoa(int(resp.GetUnilateralExitDelay())),
|
||||
utils.EXPLORER: explorer,
|
||||
utils.BOARDING_TEMPLATE: resp.GetBoardingDescriptorTemplate(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
package covenant
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
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/tree"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/vulpemventures/go-elements/payment"
|
||||
"github.com/vulpemventures/go-elements/psetv2"
|
||||
)
|
||||
|
||||
const minRelayFee = 30
|
||||
|
||||
func (c *covenantLiquidCLI) Onboard(ctx *cli.Context) error {
|
||||
amount := ctx.Uint64("amount")
|
||||
|
||||
if amount <= 0 {
|
||||
return fmt.Errorf("missing amount flag (--amount)")
|
||||
}
|
||||
|
||||
net, err := utils.GetNetwork(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userPubKey, err := utils.GetWalletPublicKey(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, cancel, err := getClientFromState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
aspPubkey, err := utils.GetAspPublicKey(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
roundLifetime, err := utils.GetRoundLifetime(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
unilateralExitDelay, err := utils.GetUnilateralExitDelay(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
congestionTreeLeaf := tree.Receiver{
|
||||
Pubkey: hex.EncodeToString(userPubKey.SerializeCompressed()),
|
||||
Amount: amount,
|
||||
}
|
||||
|
||||
liquidNet := toElementsNetwork(net)
|
||||
|
||||
treeFactoryFn, sharedOutputScript, sharedOutputAmount, err := tree.CraftCongestionTree(
|
||||
liquidNet.AssetID, aspPubkey, []tree.Receiver{congestionTreeLeaf},
|
||||
minRelayFee, roundLifetime, unilateralExitDelay,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pay, err := payment.FromScript(sharedOutputScript, &liquidNet, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
address, err := pay.TaprootAddress()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
onchainReceiver := receiver{
|
||||
To: address,
|
||||
Amount: sharedOutputAmount,
|
||||
}
|
||||
|
||||
pset, err := sendOnchain(ctx, []receiver{onchainReceiver})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ptx, _ := psetv2.NewPsetFromBase64(pset)
|
||||
utx, _ := ptx.UnsignedTx()
|
||||
txid := utx.TxHash().String()
|
||||
|
||||
congestionTree, err := treeFactoryFn(psetv2.InputArgs{
|
||||
Txid: txid,
|
||||
TxIndex: 0,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = client.Onboard(ctx.Context, &arkv1.OnboardRequest{
|
||||
BoardingTx: pset,
|
||||
CongestionTree: castCongestionTree(congestionTree),
|
||||
UserPubkey: hex.EncodeToString(userPubKey.SerializeCompressed()),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("onboard_txid:", txid)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -78,8 +78,12 @@ func collaborativeRedeem(
|
||||
|
||||
for _, coin := range selectedCoins {
|
||||
inputs = append(inputs, &arkv1.Input{
|
||||
Txid: coin.txid,
|
||||
Vout: coin.vout,
|
||||
Input: &arkv1.Input_VtxoInput{
|
||||
VtxoInput: &arkv1.VtxoInput{
|
||||
Txid: coin.txid,
|
||||
Vout: coin.vout,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -108,6 +112,7 @@ func collaborativeRedeem(
|
||||
client,
|
||||
registerResponse.GetId(),
|
||||
selectedCoins,
|
||||
false,
|
||||
secKey,
|
||||
receivers,
|
||||
)
|
||||
|
||||
@@ -143,8 +143,12 @@ func sendOffchain(ctx *cli.Context, receivers []receiver) error {
|
||||
|
||||
for _, coin := range selectedCoins {
|
||||
inputs = append(inputs, &arkv1.Input{
|
||||
Txid: coin.txid,
|
||||
Vout: coin.vout,
|
||||
Input: &arkv1.Input_VtxoInput{
|
||||
VtxoInput: &arkv1.VtxoInput{
|
||||
Txid: coin.txid,
|
||||
Vout: coin.vout,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -170,7 +174,7 @@ func sendOffchain(ctx *cli.Context, receivers []receiver) error {
|
||||
|
||||
poolTxID, err := handleRoundStream(
|
||||
ctx, client, registerResponse.GetId(),
|
||||
selectedCoins, secKey, receiversOutput,
|
||||
selectedCoins, false, secKey, receiversOutput,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -6,14 +6,11 @@ import (
|
||||
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/vulpemventures/go-elements/address"
|
||||
"github.com/vulpemventures/go-elements/payment"
|
||||
"github.com/vulpemventures/go-elements/psetv2"
|
||||
"github.com/vulpemventures/go-elements/transaction"
|
||||
)
|
||||
@@ -50,13 +47,7 @@ func signPset(
|
||||
return err
|
||||
}
|
||||
|
||||
sighashType := txscript.SigHashAll
|
||||
|
||||
if utxo.Script[0] == txscript.OP_1 {
|
||||
sighashType = txscript.SigHashDefault
|
||||
}
|
||||
|
||||
if err := updater.AddInSighashType(i, sighashType); err != nil {
|
||||
if err := updater.AddInSighashType(i, txscript.SigHashDefault); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -66,16 +57,6 @@ func signPset(
|
||||
return err
|
||||
}
|
||||
|
||||
_, onchainAddr, _, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
onchainWalletScript, err := address.ToOutputScript(onchainAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
utx, err := pset.UnsignedTx()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -99,34 +80,6 @@ func signPset(
|
||||
liquidNet := toElementsNetwork(net)
|
||||
|
||||
for i, input := range pset.Inputs {
|
||||
if bytes.Equal(input.WitnessUtxo.Script, onchainWalletScript) {
|
||||
p, err := payment.FromScript(input.WitnessUtxo.Script, &liquidNet, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
preimage := utx.HashForWitnessV0(
|
||||
i,
|
||||
p.Script,
|
||||
input.WitnessUtxo.Value,
|
||||
txscript.SigHashAll,
|
||||
)
|
||||
|
||||
sig := ecdsa.Sign(
|
||||
prvKey,
|
||||
preimage[:],
|
||||
)
|
||||
|
||||
signatureWithSighashType := append(sig.Serialize(), byte(txscript.SigHashAll))
|
||||
|
||||
err = signer.SignInput(i, signatureWithSighashType, prvKey.PubKey().SerializeCompressed(), nil, nil)
|
||||
if err != nil {
|
||||
fmt.Println("error signing input: ", err)
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if len(input.TapLeafScript) > 0 {
|
||||
genesis, err := chainhash.NewHashFromStr(liquidNet.GenesisBlockHash)
|
||||
if err != nil {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
|
||||
"github.com/ark-network/ark/client/flags"
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
@@ -21,7 +22,7 @@ func (*clArkBitcoinCLI) Balance(ctx *cli.Context) error {
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
offchainAddr, onchainAddr, redemptionAddr, err := getAddress(ctx)
|
||||
offchainAddr, boardingAddr, redemptionAddr, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -29,6 +30,21 @@ func (*clArkBitcoinCLI) Balance(ctx *cli.Context) error {
|
||||
// nolint:all
|
||||
unilateralExitDelay, _ := utils.GetUnilateralExitDelay(ctx)
|
||||
|
||||
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(3)
|
||||
|
||||
@@ -50,19 +66,19 @@ func (*clArkBitcoinCLI) Balance(ctx *cli.Context) error {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
balance, err := explorer.GetBalance(onchainAddr.EncodeAddress(), "")
|
||||
balance, lockedBalance, err := explorer.GetDelayedBalance(boardingAddr.EncodeAddress(), int64(timeoutBoarding))
|
||||
if err != nil {
|
||||
chRes <- balanceRes{0, 0, nil, nil, err}
|
||||
return
|
||||
}
|
||||
chRes <- balanceRes{0, balance, nil, nil, nil}
|
||||
chRes <- balanceRes{0, balance, lockedBalance, nil, nil}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
|
||||
spendableBalance, lockedBalance, err := explorer.GetRedeemedVtxosBalance(
|
||||
spendableBalance, lockedBalance, err := explorer.GetDelayedBalance(
|
||||
redemptionAddr.EncodeAddress(), unilateralExitDelay,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -126,6 +142,7 @@ func (*clArkBitcoinCLI) Balance(ctx *cli.Context) error {
|
||||
}
|
||||
|
||||
response := make(map[string]interface{})
|
||||
|
||||
response["onchain_balance"] = map[string]interface{}{
|
||||
"spendable_amount": onchainBalance,
|
||||
}
|
||||
|
||||
@@ -2,26 +2,62 @@ package covenantless
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
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/descriptor"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func (c *clArkBitcoinCLI) ClaimAsync(ctx *cli.Context) error {
|
||||
func (c *clArkBitcoinCLI) Claim(ctx *cli.Context) error {
|
||||
client, cancel, err := getClientFromState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
myselfOffchain, _, _, err := getAddress(ctx)
|
||||
offchainAddr, boardingAddr, _, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vtxos, err := getVtxos(ctx, nil, client, myselfOffchain, false)
|
||||
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
|
||||
boardingUtxosFromExplorer, err := explorer.GetUtxos(boardingAddr.EncodeAddress())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
boardingUtxos := make([]utils.Utxo, 0, len(boardingUtxosFromExplorer))
|
||||
for _, utxo := range boardingUtxosFromExplorer {
|
||||
u := utils.NewUtxo(utxo, uint(timeoutBoarding))
|
||||
if u.SpendableAt.Before(now) {
|
||||
continue // cannot claim if onchain spendable
|
||||
}
|
||||
|
||||
boardingUtxos = append(boardingUtxos, u)
|
||||
}
|
||||
|
||||
vtxos, err := getVtxos(ctx, nil, client, offchainAddr, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -34,32 +70,71 @@ func (c *clArkBitcoinCLI) ClaimAsync(ctx *cli.Context) error {
|
||||
pendingVtxos = append(pendingVtxos, vtxo)
|
||||
}
|
||||
}
|
||||
|
||||
for _, utxo := range boardingUtxos {
|
||||
pendingBalance += utxo.Amount
|
||||
}
|
||||
|
||||
if pendingBalance == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
receiver := receiver{
|
||||
To: myselfOffchain,
|
||||
To: offchainAddr,
|
||||
Amount: pendingBalance,
|
||||
}
|
||||
|
||||
if len(ctx.String("password")) == 0 {
|
||||
if ok := askForConfirmation(
|
||||
fmt.Sprintf(
|
||||
"claim %d satoshis from %d pending payments and %d boarding utxos",
|
||||
pendingBalance, len(pendingVtxos), len(boardingUtxos),
|
||||
),
|
||||
); !ok {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return selfTransferAllPendingPayments(
|
||||
ctx, client, pendingVtxos, receiver,
|
||||
ctx, client, pendingVtxos, boardingUtxos, receiver, boardingDescriptor,
|
||||
)
|
||||
}
|
||||
|
||||
func selfTransferAllPendingPayments(
|
||||
ctx *cli.Context, client arkv1.ArkServiceClient,
|
||||
pendingVtxos []vtxo, myself receiver,
|
||||
ctx *cli.Context,
|
||||
client arkv1.ArkServiceClient,
|
||||
pendingVtxos []vtxo,
|
||||
boardingUtxos []utils.Utxo,
|
||||
myself receiver,
|
||||
desc string,
|
||||
) error {
|
||||
inputs := make([]*arkv1.Input, 0, len(pendingVtxos))
|
||||
inputs := make([]*arkv1.Input, 0, len(pendingVtxos)+len(boardingUtxos))
|
||||
|
||||
for _, coin := range pendingVtxos {
|
||||
inputs = append(inputs, &arkv1.Input{
|
||||
Txid: coin.txid,
|
||||
Vout: coin.vout,
|
||||
Input: &arkv1.Input_VtxoInput{
|
||||
VtxoInput: &arkv1.VtxoInput{
|
||||
Txid: coin.txid,
|
||||
Vout: coin.vout,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if len(boardingUtxos) > 0 {
|
||||
for _, outpoint := range boardingUtxos {
|
||||
inputs = append(inputs, &arkv1.Input{
|
||||
Input: &arkv1.Input_BoardingInput{
|
||||
BoardingInput: &arkv1.BoardingInput{
|
||||
Txid: outpoint.Txid,
|
||||
Vout: outpoint.Vout,
|
||||
Descriptor_: desc,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
receiversOutput := []*arkv1.Output{
|
||||
{
|
||||
Address: myself.To,
|
||||
@@ -99,8 +174,8 @@ func selfTransferAllPendingPayments(
|
||||
}
|
||||
|
||||
poolTxID, err := handleRoundStream(
|
||||
ctx, client, registerResponse.GetId(),
|
||||
pendingVtxos, secKey, receiversOutput, ephemeralKey,
|
||||
ctx, client, registerResponse.GetId(), pendingVtxos,
|
||||
len(boardingUtxos) > 0, secKey, receiversOutput, ephemeralKey,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -9,10 +9,10 @@ import (
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/ark-network/ark/common/bitcointree"
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
@@ -25,14 +25,14 @@ const dust = 450
|
||||
type clArkBitcoinCLI struct{}
|
||||
|
||||
func (c *clArkBitcoinCLI) Receive(ctx *cli.Context) error {
|
||||
offchainAddr, onchainAddr, _, err := getAddress(ctx)
|
||||
offchainAddr, boardingAddr, _, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utils.PrintJSON(map[string]interface{}{
|
||||
"offchain_address": offchainAddr,
|
||||
"onchain_address": onchainAddr.EncodeAddress(),
|
||||
"boarding_address": boardingAddr.EncodeAddress(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -79,11 +79,6 @@ type receiver struct {
|
||||
Amount uint64 `json:"amount"`
|
||||
}
|
||||
|
||||
// func (r *receiver) isOnchain() bool {
|
||||
// _, err := btcutil.DecodeAddress(r.To, nil)
|
||||
// return err == nil
|
||||
// }
|
||||
|
||||
func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
|
||||
ptx, err := psbt.New(nil, nil, 2, 0, nil)
|
||||
if err != nil {
|
||||
@@ -128,14 +123,14 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
|
||||
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
|
||||
utxos, delayedUtxos, change, err := coinSelectOnchain(
|
||||
utxos, change, err := coinSelectOnchain(
|
||||
ctx, explorer, targetAmount, nil,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := addInputs(ctx, updater, utxos, delayedUtxos, &netParams); err != nil {
|
||||
if err := addInputs(ctx, updater, utxos); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -174,14 +169,14 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
|
||||
updater.Upsbt.UnsignedTx.TxOut = updater.Upsbt.UnsignedTx.TxOut[:len(updater.Upsbt.UnsignedTx.TxOut)-1]
|
||||
}
|
||||
// reselect the difference
|
||||
selected, delayedSelected, newChange, err := coinSelectOnchain(
|
||||
ctx, explorer, feeAmount-change, append(utxos, delayedUtxos...),
|
||||
selected, newChange, err := coinSelectOnchain(
|
||||
ctx, explorer, feeAmount-change, utxos,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := addInputs(ctx, updater, selected, delayedSelected, &netParams); err != nil {
|
||||
if err := addInputs(ctx, updater, selected); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -225,20 +220,37 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
|
||||
func coinSelectOnchain(
|
||||
ctx *cli.Context,
|
||||
explorer utils.Explorer, targetAmount uint64, exclude []utils.Utxo,
|
||||
) ([]utils.Utxo, []utils.Utxo, uint64, error) {
|
||||
_, onchainAddr, _, err := getAddress(ctx)
|
||||
) ([]utils.Utxo, uint64, error) {
|
||||
_, boardingAddr, redemptionAddr, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
fromExplorer, err := explorer.GetUtxos(onchainAddr.EncodeAddress())
|
||||
boardingUtxosFromExplorer, err := explorer.GetUtxos(boardingAddr.EncodeAddress())
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
utxos := make([]utils.Utxo, 0)
|
||||
selectedAmount := uint64(0)
|
||||
for _, utxo := range fromExplorer {
|
||||
now := time.Now()
|
||||
|
||||
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
for _, utxo := range boardingUtxosFromExplorer {
|
||||
if selectedAmount >= targetAmount {
|
||||
break
|
||||
}
|
||||
@@ -249,102 +261,67 @@ func coinSelectOnchain(
|
||||
}
|
||||
}
|
||||
|
||||
utxos = append(utxos, utxo)
|
||||
selectedAmount += utxo.Amount
|
||||
utxo := utils.NewUtxo(utxo, uint(timeoutBoarding))
|
||||
|
||||
if utxo.SpendableAt.After(now) {
|
||||
utxos = append(utxos, utxo)
|
||||
selectedAmount += utxo.Amount
|
||||
}
|
||||
}
|
||||
|
||||
if selectedAmount >= targetAmount {
|
||||
return utxos, nil, selectedAmount - targetAmount, nil
|
||||
return utxos, selectedAmount - targetAmount, nil
|
||||
}
|
||||
|
||||
userPubkey, err := utils.GetWalletPublicKey(ctx)
|
||||
redemptionUtxosFromExplorer, err := explorer.GetUtxos(redemptionAddr.EncodeAddress())
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
aspPubkey, err := utils.GetAspPublicKey(ctx)
|
||||
vtxoExitDelay, err := utils.GetUnilateralExitDelay(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
unilateralExitDelay, err := utils.GetUnilateralExitDelay(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
vtxoTapKey, _, err := computeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, uint(unilateralExitDelay),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
net, err := utils.GetNetwork(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
liquidNet := toChainParams(net)
|
||||
|
||||
p2tr, err := btcutil.NewAddressTaproot(
|
||||
schnorr.SerializePubKey(vtxoTapKey),
|
||||
&liquidNet,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
addr := p2tr.EncodeAddress()
|
||||
|
||||
fromExplorer, err = explorer.GetUtxos(addr)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
delayedUtxos := make([]utils.Utxo, 0)
|
||||
for _, utxo := range fromExplorer {
|
||||
for _, utxo := range redemptionUtxosFromExplorer {
|
||||
if selectedAmount >= targetAmount {
|
||||
break
|
||||
}
|
||||
|
||||
availableAt := time.Unix(utxo.Status.Blocktime, 0).Add(
|
||||
time.Duration(unilateralExitDelay) * time.Second,
|
||||
)
|
||||
if availableAt.After(time.Now()) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, excluded := range exclude {
|
||||
if utxo.Txid == excluded.Txid && utxo.Vout == excluded.Vout {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
delayedUtxos = append(delayedUtxos, utxo)
|
||||
selectedAmount += utxo.Amount
|
||||
utxo := utils.NewUtxo(utxo, uint(vtxoExitDelay))
|
||||
|
||||
if utxo.SpendableAt.After(now) {
|
||||
utxos = append(utxos, utxo)
|
||||
selectedAmount += utxo.Amount
|
||||
}
|
||||
}
|
||||
|
||||
if selectedAmount < targetAmount {
|
||||
return nil, nil, 0, fmt.Errorf(
|
||||
return nil, 0, fmt.Errorf(
|
||||
"not enough funds to cover amount %d", targetAmount,
|
||||
)
|
||||
}
|
||||
|
||||
return utxos, delayedUtxos, selectedAmount - targetAmount, nil
|
||||
return utxos, selectedAmount - targetAmount, nil
|
||||
}
|
||||
|
||||
func addInputs(
|
||||
ctx *cli.Context,
|
||||
updater *psbt.Updater,
|
||||
utxos, delayedUtxos []utils.Utxo,
|
||||
net *chaincfg.Params,
|
||||
utxos []utils.Utxo,
|
||||
) error {
|
||||
_, onchainAddr, _, err := getAddress(ctx)
|
||||
userPubkey, err := utils.GetWalletPublicKey(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
changeScript, err := txscript.PayToAddrScript(onchainAddr)
|
||||
aspPubkey, err := utils.GetAspPublicKey(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -355,87 +332,41 @@ func addInputs(
|
||||
return err
|
||||
}
|
||||
|
||||
sequence, err := utxo.Sequence()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updater.Upsbt.UnsignedTx.AddTxIn(&wire.TxIn{
|
||||
PreviousOutPoint: wire.OutPoint{
|
||||
Hash: *previousHash,
|
||||
Index: utxo.Vout,
|
||||
},
|
||||
Sequence: sequence,
|
||||
})
|
||||
|
||||
updater.Upsbt.Inputs = append(updater.Upsbt.Inputs, psbt.PInput{})
|
||||
|
||||
if err := updater.AddInWitnessUtxo(
|
||||
&wire.TxOut{
|
||||
Value: int64(utxo.Amount),
|
||||
PkScript: changeScript,
|
||||
},
|
||||
len(updater.Upsbt.UnsignedTx.TxIn)-1,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(delayedUtxos) > 0 {
|
||||
userPubkey, err := utils.GetWalletPublicKey(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aspPubkey, err := utils.GetAspPublicKey(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
unilateralExitDelay, err := utils.GetUnilateralExitDelay(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vtxoTapKey, leafProof, err := computeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, uint(unilateralExitDelay),
|
||||
_, leafProof, err := computeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, utxo.Delay,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p2tr, err := btcutil.NewAddressTaproot(schnorr.SerializePubKey(vtxoTapKey), net)
|
||||
controlBlock := leafProof.ToControlBlock(bitcointree.UnspendableKey())
|
||||
controlBlockBytes, err := controlBlock.ToBytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
script, err := txscript.PayToAddrScript(p2tr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, utxo := range delayedUtxos {
|
||||
previousHash, err := chainhash.NewHashFromStr(utxo.Txid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := addVtxoInput(
|
||||
updater,
|
||||
&wire.OutPoint{
|
||||
Hash: *previousHash,
|
||||
Index: utxo.Vout,
|
||||
updater.Upsbt.Inputs = append(updater.Upsbt.Inputs, psbt.PInput{
|
||||
TaprootLeafScript: []*psbt.TaprootTapLeafScript{
|
||||
{
|
||||
ControlBlock: controlBlockBytes,
|
||||
Script: leafProof.Script,
|
||||
LeafVersion: leafProof.LeafVersion,
|
||||
},
|
||||
uint(unilateralExitDelay),
|
||||
leafProof,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := updater.AddInWitnessUtxo(
|
||||
&wire.TxOut{
|
||||
Value: int64(utxo.Amount),
|
||||
PkScript: script,
|
||||
},
|
||||
len(updater.Upsbt.Inputs)-1,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -461,40 +392,7 @@ func decodeReceiverAddress(addr string) (
|
||||
return true, pkscript, nil, nil
|
||||
}
|
||||
|
||||
func addVtxoInput(
|
||||
updater *psbt.Updater, inputArgs *wire.OutPoint, exitDelay uint,
|
||||
tapLeafProof *txscript.TapscriptProof,
|
||||
) error {
|
||||
sequence, err := common.BIP68EncodeAsNumber(exitDelay)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
nextInputIndex := len(updater.Upsbt.Inputs)
|
||||
updater.Upsbt.UnsignedTx.AddTxIn(&wire.TxIn{
|
||||
PreviousOutPoint: *inputArgs,
|
||||
Sequence: sequence,
|
||||
})
|
||||
updater.Upsbt.Inputs = append(updater.Upsbt.Inputs, psbt.PInput{})
|
||||
|
||||
controlBlock := tapLeafProof.ToControlBlock(bitcointree.UnspendableKey())
|
||||
controlBlockBytes, err := controlBlock.ToBytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updater.Upsbt.Inputs[nextInputIndex].TaprootLeafScript = []*psbt.TaprootTapLeafScript{
|
||||
{
|
||||
ControlBlock: controlBlockBytes,
|
||||
Script: tapLeafProof.Script,
|
||||
LeafVersion: tapLeafProof.LeafVersion,
|
||||
},
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getAddress(ctx *cli.Context) (offchainAddr string, onchainAddr, redemptionAddr btcutil.Address, err error) {
|
||||
func getAddress(ctx *cli.Context) (offchainAddr string, boardingAddr, redemptionAddr btcutil.Address, err error) {
|
||||
userPubkey, err := utils.GetWalletPublicKey(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -510,6 +408,21 @@ func getAddress(ctx *cli.Context) (offchainAddr string, onchainAddr, redemptionA
|
||||
return
|
||||
}
|
||||
|
||||
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
arkNet, err := utils.GetNetwork(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -522,11 +435,6 @@ func getAddress(ctx *cli.Context) (offchainAddr string, onchainAddr, redemptionA
|
||||
|
||||
netParams := toChainParams(arkNet)
|
||||
|
||||
p2wpkh, err := btcutil.NewAddressWitnessPubKeyHash(btcutil.Hash160(userPubkey.SerializeCompressed()), &netParams)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
vtxoTapKey, _, err := computeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, uint(unilateralExitDelay),
|
||||
)
|
||||
@@ -534,7 +442,7 @@ func getAddress(ctx *cli.Context) (offchainAddr string, onchainAddr, redemptionA
|
||||
return
|
||||
}
|
||||
|
||||
p2tr, err := btcutil.NewAddressTaproot(
|
||||
redemptionP2TR, err := btcutil.NewAddressTaproot(
|
||||
schnorr.SerializePubKey(vtxoTapKey),
|
||||
&netParams,
|
||||
)
|
||||
@@ -542,8 +450,20 @@ func getAddress(ctx *cli.Context) (offchainAddr string, onchainAddr, redemptionA
|
||||
return
|
||||
}
|
||||
|
||||
redemptionAddr = p2tr
|
||||
onchainAddr = p2wpkh
|
||||
boardingTapKey, _, err := computeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, uint(timeoutBoarding),
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
boardingP2TR, err := btcutil.NewAddressTaproot(
|
||||
schnorr.SerializePubKey(boardingTapKey),
|
||||
&netParams,
|
||||
)
|
||||
|
||||
redemptionAddr = redemptionP2TR
|
||||
boardingAddr = boardingP2TR
|
||||
offchainAddr = arkAddr
|
||||
|
||||
return
|
||||
|
||||
@@ -53,14 +53,16 @@ func getVtxos(
|
||||
if v.GetSwept() {
|
||||
continue
|
||||
}
|
||||
vtxos = append(vtxos, vtxo{
|
||||
amount: v.GetReceiver().GetAmount(),
|
||||
txid: v.GetOutpoint().GetTxid(),
|
||||
vout: v.GetOutpoint().GetVout(),
|
||||
poolTxid: v.GetPoolTxid(),
|
||||
expireAt: expireAt,
|
||||
pending: v.GetPending(),
|
||||
})
|
||||
if v.Outpoint.GetVtxoInput() != nil {
|
||||
vtxos = append(vtxos, vtxo{
|
||||
amount: v.Receiver.Amount,
|
||||
txid: v.Outpoint.GetVtxoInput().GetTxid(),
|
||||
vout: v.Outpoint.GetVtxoInput().GetVout(),
|
||||
poolTxid: v.PoolTxid,
|
||||
expireAt: expireAt,
|
||||
pending: v.GetPending(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if !computeExpiration {
|
||||
@@ -196,32 +198,10 @@ func toCongestionTree(treeFromProto *arkv1.Tree) (tree.CongestionTree, error) {
|
||||
return levels, nil
|
||||
}
|
||||
|
||||
// castCongestionTree converts a tree.CongestionTree to a repeated arkv1.TreeLevel
|
||||
func castCongestionTree(congestionTree tree.CongestionTree) *arkv1.Tree {
|
||||
levels := make([]*arkv1.TreeLevel, 0, len(congestionTree))
|
||||
for _, level := range congestionTree {
|
||||
levelProto := &arkv1.TreeLevel{
|
||||
Nodes: make([]*arkv1.Node, 0, len(level)),
|
||||
}
|
||||
|
||||
for _, node := range level {
|
||||
levelProto.Nodes = append(levelProto.Nodes, &arkv1.Node{
|
||||
Txid: node.Txid,
|
||||
Tx: node.Tx,
|
||||
ParentTxid: node.ParentTxid,
|
||||
})
|
||||
}
|
||||
|
||||
levels = append(levels, levelProto)
|
||||
}
|
||||
return &arkv1.Tree{
|
||||
Levels: levels,
|
||||
}
|
||||
}
|
||||
|
||||
func handleRoundStream(
|
||||
ctx *cli.Context, client arkv1.ArkServiceClient, paymentID string,
|
||||
vtxosToSign []vtxo, secKey *secp256k1.PrivateKey, receivers []*arkv1.Output,
|
||||
vtxosToSign []vtxo, mustSignRoundTx bool,
|
||||
secKey *secp256k1.PrivateKey, receivers []*arkv1.Output,
|
||||
ephemeralKey *secp256k1.PrivateKey,
|
||||
) (poolTxID string, err error) {
|
||||
stream, err := client.GetEventStream(ctx.Context, &arkv1.GetEventStreamRequest{})
|
||||
@@ -398,8 +378,8 @@ func handleRoundStream(
|
||||
// stop pinging as soon as we receive some forfeit txs
|
||||
pingStop()
|
||||
|
||||
poolTx := e.GetPoolTx()
|
||||
ptx, err := psbt.NewFromRawBytes(strings.NewReader(poolTx), true)
|
||||
roundTx := e.GetPoolTx()
|
||||
ptx, err := psbt.NewFromRawBytes(strings.NewReader(roundTx), true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -428,7 +408,7 @@ func handleRoundStream(
|
||||
|
||||
if !isOnchainOnly(receivers) {
|
||||
if err := bitcointree.ValidateCongestionTree(
|
||||
congestionTree, poolTx, aspPubkey, int64(roundLifetime), int64(minRelayFee),
|
||||
congestionTree, roundTx, aspPubkey, int64(roundLifetime), int64(minRelayFee),
|
||||
); err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -529,80 +509,103 @@ func handleRoundStream(
|
||||
|
||||
fmt.Println("congestion tree validated")
|
||||
|
||||
forfeits := e.GetForfeitTxs()
|
||||
signedForfeits := make([]string, 0)
|
||||
|
||||
fmt.Print("signing forfeit txs... ")
|
||||
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
|
||||
connectorsTxids := make([]string, 0, len(connectors))
|
||||
for _, connector := range connectors {
|
||||
p, err := psbt.NewFromRawBytes(strings.NewReader(connector), true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
txid := p.UnsignedTx.TxHash().String()
|
||||
finalizePaymentRequest := &arkv1.FinalizePaymentRequest{}
|
||||
|
||||
connectorsTxids = append(connectorsTxids, txid)
|
||||
}
|
||||
if len(vtxosToSign) > 0 {
|
||||
forfeits := e.GetForfeitTxs()
|
||||
signedForfeits := make([]string, 0)
|
||||
|
||||
for _, forfeit := range forfeits {
|
||||
ptx, err := psbt.NewFromRawBytes(strings.NewReader(forfeit), true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
fmt.Print("signing forfeit txs... ")
|
||||
|
||||
connectorsTxids := make([]string, 0, len(connectors))
|
||||
for _, connector := range connectors {
|
||||
p, err := psbt.NewFromRawBytes(strings.NewReader(connector), true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
txid := p.UnsignedTx.TxHash().String()
|
||||
|
||||
connectorsTxids = append(connectorsTxids, txid)
|
||||
}
|
||||
|
||||
for _, input := range ptx.UnsignedTx.TxIn {
|
||||
inputTxid := input.PreviousOutPoint.Hash.String()
|
||||
for _, forfeit := range forfeits {
|
||||
ptx, err := psbt.NewFromRawBytes(strings.NewReader(forfeit), true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, coin := range vtxosToSign {
|
||||
// check if it contains one of the input to sign
|
||||
if inputTxid == coin.txid {
|
||||
// verify that the connector is in the connectors list
|
||||
connectorTxid := ptx.UnsignedTx.TxIn[0].PreviousOutPoint.Hash.String()
|
||||
connectorFound := false
|
||||
for _, txid := range connectorsTxids {
|
||||
if txid == connectorTxid {
|
||||
connectorFound = true
|
||||
break
|
||||
for _, input := range ptx.UnsignedTx.TxIn {
|
||||
inputTxid := input.PreviousOutPoint.Hash.String()
|
||||
|
||||
for _, coin := range vtxosToSign {
|
||||
// check if it contains one of the input to sign
|
||||
if inputTxid == coin.txid {
|
||||
// verify that the connector is in the connectors list
|
||||
connectorTxid := ptx.UnsignedTx.TxIn[0].PreviousOutPoint.Hash.String()
|
||||
connectorFound := false
|
||||
for _, txid := range connectorsTxids {
|
||||
if txid == connectorTxid {
|
||||
connectorFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !connectorFound {
|
||||
return "", fmt.Errorf("connector txid %s not found in the connectors list", connectorTxid)
|
||||
}
|
||||
if !connectorFound {
|
||||
return "", fmt.Errorf("connector txid %s not found in the connectors list", connectorTxid)
|
||||
}
|
||||
|
||||
if err := signPsbt(ctx, ptx, explorer, secKey); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := signPsbt(ctx, ptx, explorer, secKey); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
signedPset, err := ptx.B64Encode()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
signedPset, err := ptx.B64Encode()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
signedForfeits = append(signedForfeits, signedPset)
|
||||
signedForfeits = append(signedForfeits, signedPset)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if no forfeit txs have been signed, start pinging again and wait for the next round
|
||||
if len(signedForfeits) == 0 {
|
||||
fmt.Printf("\nno forfeit txs to sign, waiting for the next round...\n")
|
||||
pingStop = nil
|
||||
for pingStop == nil {
|
||||
pingStop = ping(ctx.Context, client, pingReq)
|
||||
// if no forfeit txs have been signed, start pinging again and wait for the next round
|
||||
if len(vtxosToSign) > 0 && len(signedForfeits) == 0 {
|
||||
fmt.Printf("\nno forfeit txs to sign, waiting for the next round...\n")
|
||||
pingStop = nil
|
||||
for pingStop == nil {
|
||||
pingStop = ping(ctx.Context, client, pingReq)
|
||||
}
|
||||
continue
|
||||
}
|
||||
continue
|
||||
|
||||
fmt.Printf("%d signed\n", len(signedForfeits))
|
||||
finalizePaymentRequest.SignedForfeitTxs = signedForfeits
|
||||
}
|
||||
|
||||
if mustSignRoundTx {
|
||||
ptx, err := psbt.NewFromRawBytes(strings.NewReader(roundTx), true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := signPsbt(ctx, ptx, explorer, secKey); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
signedRoundTx, err := ptx.B64Encode()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fmt.Println("round tx signed")
|
||||
finalizePaymentRequest.SignedRoundTx = &signedRoundTx
|
||||
}
|
||||
|
||||
fmt.Printf("%d signed\n", len(signedForfeits))
|
||||
fmt.Print("finalizing payment... ")
|
||||
_, err = client.FinalizePayment(ctx.Context, &arkv1.FinalizePaymentRequest{
|
||||
SignedForfeitTxs: signedForfeits,
|
||||
})
|
||||
_, err = client.FinalizePayment(ctx.Context, finalizePaymentRequest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ func connectToAsp(ctx *cli.Context, net, url, explorer string) error {
|
||||
utils.UNILATERAL_EXIT_DELAY: strconv.Itoa(int(resp.GetUnilateralExitDelay())),
|
||||
utils.MIN_RELAY_FEE: strconv.Itoa(int(resp.MinRelayFee)),
|
||||
utils.EXPLORER: explorer,
|
||||
utils.BOARDING_TEMPLATE: resp.GetBoardingDescriptorTemplate(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
package covenantless
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"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/bitcointree"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func (c *clArkBitcoinCLI) Onboard(ctx *cli.Context) error {
|
||||
amount := ctx.Uint64("amount")
|
||||
|
||||
if amount <= 0 {
|
||||
return fmt.Errorf("missing amount flag (--amount)")
|
||||
}
|
||||
|
||||
net, err := utils.GetNetwork(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userPubKey, err := utils.GetWalletPublicKey(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, cancel, err := getClientFromState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
aspPubkey, err := utils.GetAspPublicKey(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
roundLifetime, err := utils.GetRoundLifetime(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
unilateralExitDelay, err := utils.GetUnilateralExitDelay(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
minRelayFee, err := utils.GetMinRelayFee(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
congestionTreeLeaf := bitcointree.Receiver{
|
||||
Pubkey: hex.EncodeToString(userPubKey.SerializeCompressed()),
|
||||
Amount: uint64(amount), // Convert amount to uint64
|
||||
}
|
||||
|
||||
leaves := []bitcointree.Receiver{congestionTreeLeaf}
|
||||
|
||||
ephemeralKey, err := secp256k1.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cosigners := []*secp256k1.PublicKey{ephemeralKey.PubKey()} // TODO asp as cosigner
|
||||
|
||||
sharedOutputScript, sharedOutputAmount, err := bitcointree.CraftSharedOutput(
|
||||
cosigners,
|
||||
aspPubkey,
|
||||
leaves,
|
||||
uint64(minRelayFee),
|
||||
roundLifetime,
|
||||
unilateralExitDelay,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
netParams := toChainParams(net)
|
||||
|
||||
address, err := btcutil.NewAddressTaproot(sharedOutputScript[2:], &netParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
onchainReceiver := receiver{
|
||||
To: address.EncodeAddress(),
|
||||
Amount: uint64(sharedOutputAmount),
|
||||
}
|
||||
|
||||
partialTx, err := sendOnchain(ctx, []receiver{onchainReceiver})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ptx, err := psbt.NewFromRawBytes(strings.NewReader(partialTx), true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
txid := ptx.UnsignedTx.TxHash().String()
|
||||
|
||||
congestionTree, err := bitcointree.CraftCongestionTree(
|
||||
&wire.OutPoint{
|
||||
Hash: ptx.UnsignedTx.TxHash(),
|
||||
Index: 0,
|
||||
},
|
||||
cosigners,
|
||||
aspPubkey,
|
||||
leaves,
|
||||
uint64(minRelayFee),
|
||||
roundLifetime,
|
||||
unilateralExitDelay,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sweepClosure := bitcointree.CSVSigClosure{
|
||||
Pubkey: aspPubkey,
|
||||
Seconds: uint(roundLifetime),
|
||||
}
|
||||
|
||||
sweepTapLeaf, err := sweepClosure.Leaf()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sweepTapTree := txscript.AssembleTaprootScriptTree(*sweepTapLeaf)
|
||||
root := sweepTapTree.RootNode.TapHash()
|
||||
|
||||
signer := bitcointree.NewTreeSignerSession(
|
||||
ephemeralKey,
|
||||
congestionTree,
|
||||
minRelayFee,
|
||||
root.CloneBytes(),
|
||||
)
|
||||
|
||||
nonces, err := signer.GetNonces() // TODO send nonces to ASP
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
coordinator, err := bitcointree.NewTreeCoordinatorSession(
|
||||
congestionTree,
|
||||
minRelayFee,
|
||||
root.CloneBytes(),
|
||||
cosigners,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := coordinator.AddNonce(ephemeralKey.PubKey(), nonces); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aggregatedNonces, err := coordinator.AggregateNonces()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := signer.SetKeys(cosigners); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := signer.SetAggregatedNonces(aggregatedNonces); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sigs, err := signer.Sign()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := coordinator.AddSig(ephemeralKey.PubKey(), sigs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signedTree, err := coordinator.SignTree()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = client.Onboard(ctx.Context, &arkv1.OnboardRequest{
|
||||
BoardingTx: partialTx,
|
||||
CongestionTree: castCongestionTree(signedTree),
|
||||
UserPubkey: hex.EncodeToString(userPubKey.SerializeCompressed()),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("onboard_txid:", txid)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -72,8 +72,12 @@ func collaborativeRedeem(
|
||||
|
||||
for _, coin := range selectedCoins {
|
||||
inputs = append(inputs, &arkv1.Input{
|
||||
Txid: coin.txid,
|
||||
Vout: coin.vout,
|
||||
Input: &arkv1.Input_VtxoInput{
|
||||
VtxoInput: &arkv1.VtxoInput{
|
||||
Txid: coin.txid,
|
||||
Vout: coin.vout,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -110,6 +114,7 @@ func collaborativeRedeem(
|
||||
client,
|
||||
registerResponse.GetId(),
|
||||
selectedCoins,
|
||||
false,
|
||||
secKey,
|
||||
receivers,
|
||||
ephemeralKey,
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
func (c *clArkBitcoinCLI) SendAsync(ctx *cli.Context) error {
|
||||
receiver := ctx.String("to")
|
||||
receiverAddr := ctx.String("to")
|
||||
amount := ctx.Uint64("amount")
|
||||
withExpiryCoinselect := ctx.Bool("enable-expiry-coinselect")
|
||||
|
||||
@@ -22,15 +22,22 @@ func (c *clArkBitcoinCLI) SendAsync(ctx *cli.Context) error {
|
||||
return fmt.Errorf("invalid amount (%d), must be greater than dust %d", amount, dust)
|
||||
}
|
||||
|
||||
if receiver == "" {
|
||||
if receiverAddr == "" {
|
||||
return fmt.Errorf("receiver address is required")
|
||||
}
|
||||
isOnchain, _, _, err := decodeReceiverAddress(receiver)
|
||||
isOnchain, _, _, err := decodeReceiverAddress(receiverAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isOnchain {
|
||||
return fmt.Errorf("receiver address is onchain")
|
||||
txid, err := sendOnchain(ctx, []receiver{{receiverAddr, amount}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utils.PrintJSON(map[string]interface{}{
|
||||
"txid": txid,
|
||||
})
|
||||
}
|
||||
|
||||
offchainAddr, _, _, err := getAddress(ctx)
|
||||
@@ -41,20 +48,20 @@ func (c *clArkBitcoinCLI) SendAsync(ctx *cli.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _, aspKey, err := common.DecodeAddress(receiver)
|
||||
_, _, 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", receiver)
|
||||
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: receiver,
|
||||
Address: receiverAddr,
|
||||
Amount: amount,
|
||||
})
|
||||
sumOfReceivers += amount
|
||||
@@ -88,8 +95,12 @@ func (c *clArkBitcoinCLI) SendAsync(ctx *cli.Context) error {
|
||||
|
||||
for _, coin := range selectedCoins {
|
||||
inputs = append(inputs, &arkv1.Input{
|
||||
Txid: coin.txid,
|
||||
Vout: coin.vout,
|
||||
Input: &arkv1.Input_VtxoInput{
|
||||
VtxoInput: &arkv1.VtxoInput{
|
||||
Txid: coin.txid,
|
||||
Vout: coin.vout,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/ark-network/ark/common/bitcointree"
|
||||
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
@@ -18,7 +17,7 @@ import (
|
||||
)
|
||||
|
||||
func signPsbt(
|
||||
ctx *cli.Context, ptx *psbt.Packet, explorer utils.Explorer, prvKey *secp256k1.PrivateKey,
|
||||
_ *cli.Context, ptx *psbt.Packet, explorer utils.Explorer, prvKey *secp256k1.PrivateKey,
|
||||
) error {
|
||||
updater, err := psbt.NewUpdater(ptx)
|
||||
if err != nil {
|
||||
@@ -50,27 +49,11 @@ func signPsbt(
|
||||
return err
|
||||
}
|
||||
|
||||
sighashType := txscript.SigHashAll
|
||||
|
||||
if utxo.PkScript[0] == txscript.OP_1 {
|
||||
sighashType = txscript.SigHashDefault
|
||||
}
|
||||
|
||||
if err := updater.AddInSighashType(sighashType, i); err != nil {
|
||||
if err := updater.AddInSighashType(txscript.SigHashDefault, i); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, onchainAddr, _, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
onchainWalletScript, err := txscript.PayToAddrScript(onchainAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prevouts := make(map[wire.OutPoint]*wire.TxOut)
|
||||
|
||||
for i, input := range updater.Upsbt.Inputs {
|
||||
@@ -85,40 +68,6 @@ func signPsbt(
|
||||
txsighashes := txscript.NewTxSigHashes(updater.Upsbt.UnsignedTx, prevoutFetcher)
|
||||
|
||||
for i, input := range ptx.Inputs {
|
||||
if bytes.Equal(input.WitnessUtxo.PkScript, onchainWalletScript) {
|
||||
if err := updater.AddInSighashType(txscript.SigHashAll, i); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
preimage, err := txscript.CalcWitnessSigHash(
|
||||
input.WitnessUtxo.PkScript,
|
||||
txsighashes,
|
||||
txscript.SigHashAll,
|
||||
updater.Upsbt.UnsignedTx,
|
||||
i,
|
||||
int64(input.WitnessUtxo.Value),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sig := ecdsa.Sign(
|
||||
prvKey,
|
||||
preimage,
|
||||
)
|
||||
|
||||
signatureWithSighashType := append(sig.Serialize(), byte(txscript.SigHashAll))
|
||||
|
||||
updater.Upsbt.Inputs[i].PartialSigs = []*psbt.PartialSig{
|
||||
{
|
||||
PubKey: prvKey.PubKey().SerializeCompressed(),
|
||||
Signature: signatureWithSighashType,
|
||||
},
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if len(input.TaprootLeafScript) > 0 {
|
||||
pubkey := prvKey.PubKey()
|
||||
for _, leaf := range input.TaprootLeafScript {
|
||||
@@ -178,7 +127,6 @@ func signPsbt(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -21,10 +21,6 @@ var (
|
||||
Required: false,
|
||||
Hidden: true,
|
||||
}
|
||||
AmountOnboardFlag = cli.Uint64Flag{
|
||||
Name: "amount",
|
||||
Usage: "amount to onboard in sats",
|
||||
}
|
||||
ExpiryDetailsFlag = cli.BoolFlag{
|
||||
Name: "compute-expiry-details",
|
||||
Usage: "compute client-side the VTXOs expiry time",
|
||||
|
||||
@@ -2,6 +2,8 @@ module github.com/ark-network/ark/client
|
||||
|
||||
go 1.22.6
|
||||
|
||||
replace github.com/ark-network/ark/common => ../common
|
||||
|
||||
replace github.com/btcsuite/btcd/btcec/v2 => github.com/btcsuite/btcd/btcec/v2 v2.3.3
|
||||
|
||||
require (
|
||||
@@ -19,9 +21,7 @@ require (
|
||||
|
||||
require (
|
||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941 // indirect
|
||||
)
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||
github.com/ark-network/ark/api-spec v0.0.0-20240812233307-18e343b31899 h1:PJL9Pam042F790x3mMovaIIkgeKIVaWm1aFOyH0k4PY=
|
||||
github.com/ark-network/ark/api-spec v0.0.0-20240812233307-18e343b31899/go.mod h1:0B5seq/gzuGL8OZGUaO12yj73ZJKAde8L+nmLQAZ7IA=
|
||||
github.com/ark-network/ark/common v0.0.0-20240812233307-18e343b31899 h1:PxcHv+KaBdfrZCHoNYSUiCdI2wNIZ3Oxx8ZUewcEesg=
|
||||
github.com/ark-network/ark/common v0.0.0-20240812233307-18e343b31899/go.mod h1:8DYeb06Dl8onmrV09xfsdDMGv5HoVtWoKhLBLXOYHew=
|
||||
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
||||
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
|
||||
github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=
|
||||
|
||||
@@ -8,7 +8,6 @@ type CLI interface {
|
||||
Receive(ctx *cli.Context) error
|
||||
Redeem(ctx *cli.Context) error
|
||||
Send(ctx *cli.Context) error
|
||||
ClaimAsync(ctx *cli.Context) error
|
||||
Claim(ctx *cli.Context) error
|
||||
SendAsync(ctx *cli.Context) error
|
||||
Onboard(ctx *cli.Context) error
|
||||
}
|
||||
|
||||
@@ -74,19 +74,6 @@ var (
|
||||
Flags: []cli.Flag{&flags.PasswordFlag, &flags.PrivateKeyFlag, &flags.NetworkFlag, &flags.UrlFlag, &flags.ExplorerFlag},
|
||||
}
|
||||
|
||||
onboardCommand = cli.Command{
|
||||
Name: "onboard",
|
||||
Usage: "Onboard the Ark by lifting your funds",
|
||||
Action: func(ctx *cli.Context) error {
|
||||
cli, err := getCLIFromState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cli.Onboard(ctx)
|
||||
},
|
||||
Flags: []cli.Flag{&flags.AmountOnboardFlag, &flags.PasswordFlag},
|
||||
}
|
||||
|
||||
sendCommand = cli.Command{
|
||||
Name: "send",
|
||||
Usage: "Send your onchain or offchain funds to one or many receivers",
|
||||
@@ -117,7 +104,7 @@ var (
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cli.ClaimAsync(ctx)
|
||||
return cli.Claim(ctx)
|
||||
},
|
||||
Flags: []cli.Flag{&flags.PasswordFlag},
|
||||
}
|
||||
@@ -167,7 +154,6 @@ func main() {
|
||||
&redeemCommand,
|
||||
&sendCommand,
|
||||
&claimCommand,
|
||||
&onboardCommand,
|
||||
)
|
||||
app.Flags = []cli.Flag{
|
||||
flags.DatadirFlag,
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"github.com/vulpemventures/go-elements/transaction"
|
||||
)
|
||||
|
||||
type Utxo struct {
|
||||
type ExplorerUtxo struct {
|
||||
Txid string `json:"txid"`
|
||||
Vout uint32 `json:"vout"`
|
||||
Amount uint64 `json:"value"`
|
||||
@@ -32,9 +32,9 @@ type Utxo struct {
|
||||
type Explorer interface {
|
||||
GetTxHex(txid string) (string, error)
|
||||
Broadcast(txHex string) (string, error)
|
||||
GetUtxos(addr string) ([]Utxo, error)
|
||||
GetUtxos(addr string) ([]ExplorerUtxo, error)
|
||||
GetBalance(addr, asset string) (uint64, error)
|
||||
GetRedeemedVtxosBalance(
|
||||
GetDelayedBalance(
|
||||
addr string, unilateralExitDelay int64,
|
||||
) (uint64, map[int64]uint64, error)
|
||||
GetTxBlocktime(txid string) (confirmed bool, blocktime int64, err error)
|
||||
@@ -130,7 +130,7 @@ func (e *explorer) Broadcast(txStr string) (string, error) {
|
||||
return txid, nil
|
||||
}
|
||||
|
||||
func (e *explorer) GetUtxos(addr string) ([]Utxo, error) {
|
||||
func (e *explorer) GetUtxos(addr string) ([]ExplorerUtxo, error) {
|
||||
endpoint, err := url.JoinPath(e.baseUrl, "address", addr, "utxo")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -149,7 +149,7 @@ func (e *explorer) GetUtxos(addr string) ([]Utxo, error) {
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf(string(body))
|
||||
}
|
||||
payload := []Utxo{}
|
||||
payload := []ExplorerUtxo{}
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -175,7 +175,7 @@ func (e *explorer) GetBalance(addr, asset string) (uint64, error) {
|
||||
return balance, nil
|
||||
}
|
||||
|
||||
func (e *explorer) GetRedeemedVtxosBalance(
|
||||
func (e *explorer) GetDelayedBalance(
|
||||
addr string, unilateralExitDelay int64,
|
||||
) (spendableBalance uint64, lockedBalance map[int64]uint64, err error) {
|
||||
utxos, err := e.GetUtxos(addr)
|
||||
|
||||
@@ -7,8 +7,10 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -18,6 +20,7 @@ const (
|
||||
ASP_PUBKEY = "asp_public_key"
|
||||
ROUND_LIFETIME = "round_lifetime"
|
||||
UNILATERAL_EXIT_DELAY = "unilateral_exit_delay"
|
||||
BOARDING_TEMPLATE = "boarding_template"
|
||||
ENCRYPTED_PRVKEY = "encrypted_private_key"
|
||||
PASSWORD_HASH = "password_hash"
|
||||
PUBKEY = "public_key"
|
||||
@@ -109,6 +112,27 @@ func GetUnilateralExitDelay(ctx *cli.Context) (int64, error) {
|
||||
return int64(redeemDelay), nil
|
||||
}
|
||||
|
||||
func GetBoardingDescriptor(ctx *cli.Context) (string, error) {
|
||||
state, err := GetState(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pubkey, err := GetWalletPublicKey(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
template := state[BOARDING_TEMPLATE]
|
||||
if len(template) <= 0 {
|
||||
return "", fmt.Errorf("missing boarding descriptor template")
|
||||
}
|
||||
|
||||
pubkeyhex := hex.EncodeToString(schnorr.SerializePubKey(pubkey))
|
||||
|
||||
return strings.ReplaceAll(template, "USER", pubkeyhex), nil
|
||||
}
|
||||
|
||||
func GetWalletPublicKey(ctx *cli.Context) (*secp256k1.PublicKey, error) {
|
||||
state, err := GetState(ctx)
|
||||
if err != nil {
|
||||
|
||||
36
client/utils/types.go
Normal file
36
client/utils/types.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
)
|
||||
|
||||
type Utxo struct {
|
||||
Txid string
|
||||
Vout uint32
|
||||
Amount uint64
|
||||
Asset string // optional
|
||||
Delay uint
|
||||
SpendableAt time.Time
|
||||
}
|
||||
|
||||
func (u *Utxo) Sequence() (uint32, error) {
|
||||
return common.BIP68EncodeAsNumber(u.Delay)
|
||||
}
|
||||
|
||||
func NewUtxo(explorerUtxo ExplorerUtxo, delay uint) Utxo {
|
||||
utxoTime := explorerUtxo.Status.Blocktime
|
||||
if utxoTime == 0 {
|
||||
utxoTime = time.Now().Unix()
|
||||
}
|
||||
|
||||
return Utxo{
|
||||
Txid: explorerUtxo.Txid,
|
||||
Vout: explorerUtxo.Vout,
|
||||
Amount: explorerUtxo.Amount,
|
||||
Asset: explorerUtxo.Asset,
|
||||
Delay: delay,
|
||||
SpendableAt: time.Unix(utxoTime, 0).Add(time.Duration(delay) * time.Second),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user