mirror of
https://github.com/aljazceru/ark.git
synced 2025-12-17 20:24:21 +01:00
Support onboarding & Drop faucet (#119)
* Renaming * Add server-side support for onboarding * add onboard --amount command * support client side onboarding * Drop dummy tx builder * Drop faucet * Fixes * fix public key encoding * fix schnorr pub key check in validation * fix server/README to accomodate onboarding --------- Co-authored-by: Louis <louis@vulpem.com> Co-authored-by: João Bordalo <bordalix@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
a95a829b20
commit
1650ea5935
@@ -18,7 +18,7 @@ var expiryDetailsFlag = cli.BoolFlag{
|
||||
|
||||
var balanceCommand = cli.Command{
|
||||
Name: "balance",
|
||||
Usage: "Print balance of the Ark wallet",
|
||||
Usage: "Shows the onchain and offchain balance of the Ark wallet",
|
||||
Action: balanceAction,
|
||||
Flags: []cli.Flag{&expiryDetailsFlag},
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
@@ -596,7 +597,7 @@ func handleRoundStream(
|
||||
if len(output.Script) == 0 {
|
||||
continue
|
||||
}
|
||||
if bytes.Equal(output.Script[2:], outputTapKey.SerializeCompressed()) {
|
||||
if bytes.Equal(output.Script[2:], schnorr.SerializePubKey(outputTapKey)) {
|
||||
if output.Value != receiver.Amount {
|
||||
continue
|
||||
}
|
||||
@@ -733,6 +734,29 @@ 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 decodeReceiverAddress(addr string) (
|
||||
isOnChainAddress bool,
|
||||
onchainScript []byte,
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
var configCommand = cli.Command{
|
||||
Name: "config",
|
||||
Usage: "Print local configuration of the Ark CLI",
|
||||
Usage: "Shows configuration of the Ark wallet",
|
||||
Action: printConfigAction,
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,12 @@ import (
|
||||
|
||||
var dumpCommand = cli.Command{
|
||||
Name: "dump-privkey",
|
||||
Usage: "Dump private key of the Ark wallet",
|
||||
Usage: "Dumps private key of the Ark wallet",
|
||||
Action: dumpAction,
|
||||
}
|
||||
|
||||
func dumpAction(ctx *cli.Context) error {
|
||||
privateKey, err := privateKeyFromPassword()
|
||||
privateKey, err := privateKeyFromPassword()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var faucetCommand = cli.Command{
|
||||
Name: "faucet",
|
||||
Usage: "Faucet your wallet",
|
||||
Action: faucetAction,
|
||||
}
|
||||
|
||||
func faucetAction(ctx *cli.Context) error {
|
||||
addr, _, err := getAddress()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, close, err := getClientFromState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer close()
|
||||
|
||||
_, err = client.Faucet(ctx.Context, &arkv1.FaucetRequest{
|
||||
Address: addr,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
eventStream, err := client.GetEventStream(ctx.Context, &arkv1.GetEventStreamRequest{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
event, err := eventStream.Recv()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if event.GetRoundFinalization() != nil {
|
||||
if _, err := client.FinalizePayment(context.Background(), &arkv1.FinalizePaymentRequest{
|
||||
SignedForfeitTxs: event.GetRoundFinalization().GetForfeitTxs(),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if event.GetRoundFailed() != nil {
|
||||
return fmt.Errorf("faucet failed: %s", event.GetRoundFailed().GetReason())
|
||||
}
|
||||
|
||||
if event.GetRoundFinalized() != nil {
|
||||
return printJSON(map[string]interface{}{
|
||||
"pool_txid": event.GetRoundFinalized().GetPoolTxid(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -36,7 +36,7 @@ var (
|
||||
|
||||
var initCommand = cli.Command{
|
||||
Name: "init",
|
||||
Usage: "initialize the wallet with an encryption password, and connect it to an ASP",
|
||||
Usage: "Initialize your Ark wallet with an encryption password, and connect it to an ASP",
|
||||
Action: initAction,
|
||||
Flags: []cli.Flag{&passwordFlag, &privateKeyFlag, &networkFlag, &urlFlag},
|
||||
}
|
||||
|
||||
@@ -57,17 +57,17 @@ func main() {
|
||||
|
||||
app.Version = version
|
||||
app.Name = "Ark CLI"
|
||||
app.Usage = "command line interface for Ark wallet"
|
||||
app.Usage = "ark wallet command line interface"
|
||||
app.Commands = append(
|
||||
app.Commands,
|
||||
&balanceCommand,
|
||||
&configCommand,
|
||||
&dumpCommand,
|
||||
&faucetCommand,
|
||||
&initCommand,
|
||||
&receiveCommand,
|
||||
&redeemCommand,
|
||||
&sendCommand,
|
||||
&onboardCommand,
|
||||
)
|
||||
|
||||
app.Before = func(ctx *cli.Context) error {
|
||||
|
||||
147
client/onboard.go
Normal file
147
client/onboard.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
|
||||
"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
|
||||
)
|
||||
|
||||
var (
|
||||
amountOnboardFlag = cli.Uint64Flag{
|
||||
Name: "amount",
|
||||
Usage: "amount to onboard in sats",
|
||||
Required: true,
|
||||
}
|
||||
)
|
||||
|
||||
var onboardCommand = cli.Command{
|
||||
Name: "onboard",
|
||||
Usage: "Onboard the Ark by lifting your funds",
|
||||
Action: onboardAction,
|
||||
Flags: []cli.Flag{&amountOnboardFlag},
|
||||
}
|
||||
|
||||
func onboardAction(ctx *cli.Context) error {
|
||||
amount := ctx.Uint64("amount")
|
||||
|
||||
if amount <= 0 {
|
||||
return fmt.Errorf("missing amount flag (--amount)")
|
||||
}
|
||||
|
||||
_, net := getNetwork()
|
||||
|
||||
aspPubkey, err := getServiceProviderPublicKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lifetime, err := getLifetime()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
exitDelay, err := getExitDelay()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userPubKey, err := getWalletPublicKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
congestionTreeLeaf := tree.Receiver{
|
||||
Pubkey: hex.EncodeToString(userPubKey.SerializeCompressed()),
|
||||
Amount: amount,
|
||||
}
|
||||
|
||||
treeFactoryFn, sharedOutputScript, sharedOutputAmount, err := tree.CraftCongestionTree(
|
||||
net.AssetID, aspPubkey, []tree.Receiver{congestionTreeLeaf}, minRelayFee, lifetime, exitDelay,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pay, err := payment.FromScript(sharedOutputScript, net, 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
|
||||
}
|
||||
|
||||
txid, err := broadcastPset(pset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("onboard txid:", txid)
|
||||
fmt.Println("waiting for confirmation... (this may take a while, do not cancel the process)")
|
||||
|
||||
// wait for the transaction to be confirmed
|
||||
if err := waitForTxConfirmation(ctx, txid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("transaction confirmed")
|
||||
|
||||
congestionTree, err := treeFactoryFn(psetv2.InputArgs{
|
||||
Txid: txid,
|
||||
TxIndex: 0,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, close, err := getClientFromState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer close()
|
||||
|
||||
_, err = client.Onboard(ctx.Context, &arkv1.OnboardRequest{
|
||||
BoardingTx: pset,
|
||||
CongestionTree: castCongestionTree(congestionTree),
|
||||
UserPubkey: hex.EncodeToString(userPubKey.SerializeCompressed()),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func waitForTxConfirmation(ctx *cli.Context, txid string) error {
|
||||
isConfirmed := false
|
||||
|
||||
for !isConfirmed {
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
isConfirmed, _, _ = getTxBlocktime(txid)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
var receiveCommand = cli.Command{
|
||||
Name: "receive",
|
||||
Usage: "Print the Ark address associated with your wallet and the connected Ark",
|
||||
Usage: "Shows both onchain and offchain addresses",
|
||||
Action: receiveAction,
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ var (
|
||||
|
||||
var redeemCommand = cli.Command{
|
||||
Name: "redeem",
|
||||
Usage: "Redeem VTXO(s) to onchain",
|
||||
Usage: "Redeem your offchain funds, either collaboratively or unilaterally",
|
||||
Flags: []cli.Flag{&addressFlag, &amountToRedeemFlag, &forceFlag},
|
||||
Action: redeemAction,
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ var (
|
||||
|
||||
var sendCommand = cli.Command{
|
||||
Name: "send",
|
||||
Usage: "Send VTXOs to a list of addresses",
|
||||
Usage: "Send your onchain or offchain funds to one or many receivers",
|
||||
Action: sendAction,
|
||||
Flags: []cli.Flag{&receiversFlag, &toFlag, &amountFlag},
|
||||
}
|
||||
@@ -83,9 +83,19 @@ func sendAction(ctx *cli.Context) error {
|
||||
}
|
||||
|
||||
if len(onchainReceivers) > 0 {
|
||||
if err := sendOnchain(ctx, onchainReceivers); err != nil {
|
||||
pset, err := sendOnchain(ctx, onchainReceivers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
txid, err := broadcastPset(pset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return printJSON(map[string]interface{}{
|
||||
"txid": txid,
|
||||
})
|
||||
}
|
||||
|
||||
if len(offchainReceivers) > 0 {
|
||||
@@ -203,14 +213,14 @@ func sendOffchain(ctx *cli.Context, receivers []receiver) error {
|
||||
})
|
||||
}
|
||||
|
||||
func sendOnchain(ctx *cli.Context, receivers []receiver) error {
|
||||
func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
|
||||
pset, err := psetv2.New(nil, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
updater, err := psetv2.NewUpdater(pset)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, net := getNetwork()
|
||||
@@ -219,12 +229,12 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) error {
|
||||
for _, receiver := range receivers {
|
||||
targetAmount += receiver.Amount
|
||||
if receiver.Amount < DUST {
|
||||
return fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount, DUST)
|
||||
return "", fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount, DUST)
|
||||
}
|
||||
|
||||
script, err := address.ToOutputScript(receiver.To)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := updater.AddOutputs([]psetv2.OutputArgs{
|
||||
@@ -234,28 +244,28 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) error {
|
||||
Script: script,
|
||||
},
|
||||
}); err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
selected, delayedSelected, change, err := coinSelectOnchain(targetAmount, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := addInputs(updater, selected, delayedSelected, net); err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
if change > 0 {
|
||||
_, changeAddr, err := getAddress()
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
changeScript, err := address.ToOutputScript(changeAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := updater.AddOutputs([]psetv2.OutputArgs{
|
||||
@@ -265,13 +275,13 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) error {
|
||||
Script: changeScript,
|
||||
},
|
||||
}); err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
utx, err := updater.Pset.UnsignedTx()
|
||||
utx, err := pset.UnsignedTx()
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
vBytes := utx.VirtualSize()
|
||||
@@ -291,22 +301,22 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) error {
|
||||
append(selected, delayedSelected...),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := addInputs(updater, selected, delayedSelected, net); err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
if newChange > 0 {
|
||||
_, changeAddr, err := getAddress()
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
changeScript, err := address.ToOutputScript(changeAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := updater.AddOutputs([]psetv2.OutputArgs{
|
||||
@@ -316,7 +326,7 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) error {
|
||||
Script: changeScript,
|
||||
},
|
||||
}); err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -327,40 +337,42 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) error {
|
||||
Amount: feeAmount,
|
||||
},
|
||||
}); err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
prvKey, err := privateKeyFromPassword()
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
explorer := NewExplorer()
|
||||
|
||||
if err := signPset(updater.Pset, explorer, prvKey); err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := psetv2.FinalizeAll(updater.Pset); err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
return updater.Pset.ToBase64()
|
||||
}
|
||||
|
||||
func broadcastPset(psetB64 string) (string, error) {
|
||||
pset, err := psetv2.NewPsetFromBase64(psetB64)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
extracted, err := psetv2.Extract(pset)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
hex, err := extracted.ToHex()
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
txid, err := explorer.Broadcast(hex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return printJSON(map[string]interface{}{
|
||||
"txid": txid,
|
||||
})
|
||||
return NewExplorer().Broadcast(hex)
|
||||
}
|
||||
|
||||
@@ -1,22 +1,49 @@
|
||||
package txbuilder
|
||||
package tree
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/ark-network/ark/internal/core/domain"
|
||||
"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/vulpemventures/go-elements/psetv2"
|
||||
"github.com/vulpemventures/go-elements/taproot"
|
||||
)
|
||||
|
||||
type treeFactory func(outpoint psetv2.InputArgs) (tree.CongestionTree, error)
|
||||
func CraftCongestionTree(
|
||||
asset string, aspPublicKey *secp256k1.PublicKey,
|
||||
receivers []Receiver, feeSatsPerNode uint64, roundLifetime int64, exitDelay int64,
|
||||
) (
|
||||
buildCongestionTree TreeFactory,
|
||||
sharedOutputScript []byte, sharedOutputAmount uint64, err error,
|
||||
) {
|
||||
root, err := createPartialCongestionTree(
|
||||
receivers, aspPublicKey, asset, feeSatsPerNode, roundLifetime, exitDelay,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
taprootKey, _, err := root.getWitnessData()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
sharedOutputScript, err = taprootOutputScript(taprootKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
sharedOutputAmount = root.getAmount() + root.feeSats
|
||||
buildCongestionTree = root.createFinalCongestionTree()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type node struct {
|
||||
sweepKey *secp256k1.PublicKey
|
||||
receivers []domain.Receiver
|
||||
receivers []Receiver
|
||||
left *node
|
||||
right *node
|
||||
asset string
|
||||
@@ -131,7 +158,7 @@ func (n *node) getWitnessData() (
|
||||
return n._inputTaprootKey, n._inputTaprootTree, nil
|
||||
}
|
||||
|
||||
sweepClosure := &tree.CSVSigClosure{
|
||||
sweepClosure := &CSVSigClosure{
|
||||
Pubkey: n.sweepKey,
|
||||
Seconds: uint(n.roundLifetime),
|
||||
}
|
||||
@@ -147,7 +174,7 @@ func (n *node) getWitnessData() (
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
unrollClosure := &tree.UnrollClosure{
|
||||
unrollClosure := &UnrollClosure{
|
||||
LeftKey: taprootKey,
|
||||
LeftAmount: n.getAmount(),
|
||||
}
|
||||
@@ -163,7 +190,7 @@ func (n *node) getWitnessData() (
|
||||
root := branchTaprootTree.RootNode.TapHash()
|
||||
|
||||
inputTapkey := taproot.ComputeTaprootOutputKey(
|
||||
tree.UnspendableKey(),
|
||||
UnspendableKey(),
|
||||
root[:],
|
||||
)
|
||||
|
||||
@@ -186,7 +213,7 @@ func (n *node) getWitnessData() (
|
||||
leftAmount := n.left.getAmount() + n.feeSats
|
||||
rightAmount := n.right.getAmount() + n.feeSats
|
||||
|
||||
unrollClosure := &tree.UnrollClosure{
|
||||
unrollClosure := &UnrollClosure{
|
||||
LeftKey: leftKey,
|
||||
LeftAmount: leftAmount,
|
||||
RightKey: rightKey,
|
||||
@@ -204,7 +231,7 @@ func (n *node) getWitnessData() (
|
||||
root := branchTaprootTree.RootNode.TapHash()
|
||||
|
||||
taprootKey := taproot.ComputeTaprootOutputKey(
|
||||
tree.UnspendableKey(),
|
||||
UnspendableKey(),
|
||||
root[:],
|
||||
)
|
||||
|
||||
@@ -231,7 +258,7 @@ func (n *node) getVtxoWitnessData() (
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
redeemClosure := &tree.CSVSigClosure{
|
||||
redeemClosure := &CSVSigClosure{
|
||||
Pubkey: pubkey,
|
||||
Seconds: uint(n.exitDelay),
|
||||
}
|
||||
@@ -241,7 +268,7 @@ func (n *node) getVtxoWitnessData() (
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
forfeitClosure := &tree.ForfeitClosure{
|
||||
forfeitClosure := &ForfeitClosure{
|
||||
Pubkey: pubkey,
|
||||
AspPubkey: n.sweepKey,
|
||||
}
|
||||
@@ -257,7 +284,7 @@ func (n *node) getVtxoWitnessData() (
|
||||
root := leafTaprootTree.RootNode.TapHash()
|
||||
|
||||
taprootKey := taproot.ComputeTaprootOutputKey(
|
||||
tree.UnspendableKey(),
|
||||
UnspendableKey(),
|
||||
root[:],
|
||||
)
|
||||
|
||||
@@ -266,25 +293,24 @@ func (n *node) getVtxoWitnessData() (
|
||||
|
||||
func (n *node) getTreeNode(
|
||||
input psetv2.InputArgs, tapTree *taproot.IndexedElementsTapScriptTree,
|
||||
) (tree.Node, error) {
|
||||
) (Node, error) {
|
||||
pset, err := n.getTx(input, tapTree)
|
||||
if err != nil {
|
||||
return tree.Node{}, err
|
||||
return Node{}, err
|
||||
}
|
||||
|
||||
txid, err := getPsetId(pset)
|
||||
if err != nil {
|
||||
return tree.Node{}, err
|
||||
return Node{}, err
|
||||
}
|
||||
|
||||
tx, err := pset.ToBase64()
|
||||
if err != nil {
|
||||
return tree.Node{}, err
|
||||
return Node{}, err
|
||||
}
|
||||
|
||||
parentTxid := chainhash.Hash(pset.Inputs[0].PreviousTxid).String()
|
||||
|
||||
return tree.Node{
|
||||
return Node{
|
||||
Txid: txid,
|
||||
Tx: tx,
|
||||
ParentTxid: parentTxid,
|
||||
@@ -306,7 +332,7 @@ func (n *node) getTx(
|
||||
}
|
||||
|
||||
if err := addTaprootInput(
|
||||
updater, input, tree.UnspendableKey(), inputTapTree,
|
||||
updater, input, UnspendableKey(), inputTapTree,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -328,9 +354,9 @@ func (n *node) getTx(
|
||||
return pset, nil
|
||||
}
|
||||
|
||||
func (n *node) createFinalCongestionTree() treeFactory {
|
||||
return func(poolTxInput psetv2.InputArgs) (tree.CongestionTree, error) {
|
||||
congestionTree := make(tree.CongestionTree, 0)
|
||||
func (n *node) createFinalCongestionTree() TreeFactory {
|
||||
return func(poolTxInput psetv2.InputArgs) (CongestionTree, error) {
|
||||
congestionTree := make(CongestionTree, 0)
|
||||
|
||||
_, taprootTree, err := n.getWitnessData()
|
||||
if err != nil {
|
||||
@@ -346,7 +372,7 @@ func (n *node) createFinalCongestionTree() treeFactory {
|
||||
nextInputsArgs := make([]psetv2.InputArgs, 0)
|
||||
nextTaprootTrees := make([]*taproot.IndexedElementsTapScriptTree, 0)
|
||||
|
||||
treeLevel := make([]tree.Node, 0)
|
||||
treeLevel := make([]Node, 0)
|
||||
|
||||
for i, node := range nodes {
|
||||
treeNode, err := node.getTreeNode(ins[i], inTrees[i])
|
||||
@@ -385,38 +411,8 @@ func (n *node) createFinalCongestionTree() treeFactory {
|
||||
}
|
||||
}
|
||||
|
||||
func craftCongestionTree(
|
||||
asset string, aspPublicKey *secp256k1.PublicKey,
|
||||
payments []domain.Payment, feeSatsPerNode uint64, roundLifetime int64, exitDelay int64,
|
||||
) (
|
||||
buildCongestionTree treeFactory,
|
||||
sharedOutputScript []byte, sharedOutputAmount uint64, err error,
|
||||
) {
|
||||
receivers := getOffchainReceivers(payments)
|
||||
root, err := createPartialCongestionTree(
|
||||
receivers, aspPublicKey, asset, feeSatsPerNode, roundLifetime, exitDelay,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
taprootKey, _, err := root.getWitnessData()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
sharedOutputScript, err = taprootOutputScript(taprootKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
sharedOutputAmount = root.getAmount() + root.feeSats
|
||||
buildCongestionTree = root.createFinalCongestionTree()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func createPartialCongestionTree(
|
||||
receivers []domain.Receiver,
|
||||
receivers []Receiver,
|
||||
aspPublicKey *secp256k1.PublicKey,
|
||||
asset string,
|
||||
feeSatsPerNode uint64,
|
||||
@@ -431,7 +427,7 @@ func createPartialCongestionTree(
|
||||
for _, r := range receivers {
|
||||
leafNode := &node{
|
||||
sweepKey: aspPublicKey,
|
||||
receivers: []domain.Receiver{r},
|
||||
receivers: []Receiver{r},
|
||||
asset: asset,
|
||||
feeSats: feeSatsPerNode,
|
||||
roundLifetime: roundLifetime,
|
||||
@@ -478,3 +474,45 @@ func createUpperLevel(nodes []*node) ([]*node, error) {
|
||||
}
|
||||
return pairs, nil
|
||||
}
|
||||
|
||||
func taprootOutputScript(taprootKey *secp256k1.PublicKey) ([]byte, error) {
|
||||
return txscript.NewScriptBuilder().AddOp(txscript.OP_1).AddData(schnorr.SerializePubKey(taprootKey)).Script()
|
||||
}
|
||||
|
||||
func getPsetId(pset *psetv2.Pset) (string, error) {
|
||||
utx, err := pset.UnsignedTx()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return utx.TxHash().String(), nil
|
||||
}
|
||||
|
||||
// wrapper of updater methods adding a taproot input to the pset with all the necessary data to spend it via any taproot script
|
||||
func addTaprootInput(
|
||||
updater *psetv2.Updater,
|
||||
input psetv2.InputArgs,
|
||||
internalTaprootKey *secp256k1.PublicKey,
|
||||
taprootTree *taproot.IndexedElementsTapScriptTree,
|
||||
) error {
|
||||
if err := updater.AddInputs([]psetv2.InputArgs{input}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := updater.AddInTapInternalKey(0, schnorr.SerializePubKey(internalTaprootKey)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, proof := range taprootTree.LeafMerkleProofs {
|
||||
controlBlock := proof.ToControlBlock(internalTaprootKey)
|
||||
|
||||
if err := updater.AddInTapLeafScript(0, psetv2.TapLeafScript{
|
||||
TapElementsLeaf: taproot.NewBaseTapElementsLeaf(proof.Script),
|
||||
ControlBlock: controlBlock,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
10
common/tree/type.go
Normal file
10
common/tree/type.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package tree
|
||||
|
||||
import "github.com/vulpemventures/go-elements/psetv2"
|
||||
|
||||
type TreeFactory func(outpoint psetv2.InputArgs) (CongestionTree, error)
|
||||
|
||||
type Receiver struct {
|
||||
Pubkey string
|
||||
Amount uint64
|
||||
}
|
||||
@@ -236,7 +236,7 @@ func validateNodeTransaction(
|
||||
|
||||
switch c := close.(type) {
|
||||
case *CSVSigClosure:
|
||||
isASP := c.Pubkey.IsEqual(expectedPublicKeyASP)
|
||||
isASP := bytes.Equal(schnorr.SerializePubKey(c.Pubkey), schnorr.SerializePubKey(expectedPublicKeyASP))
|
||||
isSweepDelay := int64(c.Seconds) == expectedSequenceSeconds
|
||||
|
||||
if isASP && !isSweepDelay {
|
||||
|
||||
@@ -71,24 +71,36 @@ This will add a `state.json` file to the following directory:
|
||||
**Note:** you can use a different datadir by exporting the env var `ARK_WALLET_DATADIR` like:
|
||||
|
||||
```bash
|
||||
$ export ARK_WALLET_DATADIR=path/to/custom
|
||||
$ export ARK_WALLET_DATADIR=path/to/custom
|
||||
$ ark init --password <password> --ark-url localhost:6000
|
||||
```
|
||||
|
||||
Add funds to the ark wallet:
|
||||
|
||||
```
|
||||
$ ark faucet
|
||||
# ark now has 10000 sats on its offchain balance
|
||||
$ ark receive
|
||||
{
|
||||
"offchain_address": <address starting with "tark1q...">,
|
||||
"onchain_address": <address starting with "tex1q...">
|
||||
}
|
||||
```
|
||||
|
||||
Fund the `onchain_address` with https://liquidtestnet.com/faucet.
|
||||
|
||||
Onboard the ark:
|
||||
|
||||
```
|
||||
$ ark onboard --amount 21000
|
||||
```
|
||||
|
||||
After confirmation, ark wallet will be funded and ready to spend offchain.
|
||||
|
||||
In **another tab**, setup another ark wallet with:
|
||||
|
||||
```
|
||||
$ export ARK_WALLET_DATADIR=./datadir
|
||||
$ alias ark2=$(pwd)/build/ark-cli-<os>-<arch>
|
||||
$ ark2 init --password <password> --ark-url localhost:6000
|
||||
$ ark2 faucet
|
||||
# ark2 now has 10000 sats on ark
|
||||
```
|
||||
|
||||
**Note:** `ark2` should always run in the second tab.
|
||||
@@ -98,7 +110,7 @@ $ ark2 faucet
|
||||
You can now make ark payments between the 2 ark wallets:
|
||||
|
||||
```
|
||||
$ ark receive
|
||||
$ ark2 receive
|
||||
{
|
||||
"offchain_address": <address starting with "tark1q...">,
|
||||
"onchain_address": <address starting with "tex1q...">,
|
||||
@@ -109,7 +121,7 @@ $ ark receive
|
||||
```
|
||||
|
||||
```
|
||||
$ ark2 send --to <offchain_address> --amount 2100
|
||||
$ ark send --to <ark2 offchain address> --amount 2100
|
||||
```
|
||||
|
||||
Both balances should reflect the payment:
|
||||
@@ -117,15 +129,15 @@ Both balances should reflect the payment:
|
||||
```
|
||||
$ ark balance
|
||||
{
|
||||
"offchain_balance": 12100,
|
||||
"onchain_balance": 0
|
||||
"offchain_balance": 18900,
|
||||
"onchain_balance": 78872
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
$ ark2 balance
|
||||
{
|
||||
"offchain_balance": 7900,
|
||||
"offchain_balance": 2100,
|
||||
"onchain_balance": 0
|
||||
}
|
||||
```
|
||||
|
||||
@@ -47,36 +47,6 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v1/faucet/{address}": {
|
||||
"post": {
|
||||
"operationId": "ArkService_Faucet",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A successful response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1FaucetResponse"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "An unexpected error response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/rpcStatus"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "address",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"ArkService"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v1/info": {
|
||||
"get": {
|
||||
"operationId": "ArkService_GetInfo",
|
||||
@@ -99,6 +69,38 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v1/onboard": {
|
||||
"post": {
|
||||
"operationId": "ArkService_Onboard",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A successful response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1OnboardResponse"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "An unexpected error response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/rpcStatus"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1OnboardRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"ArkService"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v1/payment/claim": {
|
||||
"post": {
|
||||
"operationId": "ArkService_ClaimPayment",
|
||||
@@ -335,9 +337,6 @@
|
||||
"v1ClaimPaymentResponse": {
|
||||
"type": "object"
|
||||
},
|
||||
"v1FaucetResponse": {
|
||||
"type": "object"
|
||||
},
|
||||
"v1FinalizePaymentRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -429,6 +428,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1OnboardRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"boardingTx": {
|
||||
"type": "string"
|
||||
},
|
||||
"congestionTree": {
|
||||
"$ref": "#/definitions/v1Tree"
|
||||
},
|
||||
"userPubkey": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1OnboardResponse": {
|
||||
"type": "object"
|
||||
},
|
||||
"v1Output": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -38,11 +38,6 @@ service ArkService {
|
||||
get: "/v1/ping/{payment_id}"
|
||||
};
|
||||
};
|
||||
rpc Faucet(FaucetRequest) returns (FaucetResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v1/faucet/{address}"
|
||||
};
|
||||
}
|
||||
rpc ListVtxos(ListVtxosRequest) returns (ListVtxosResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/v1/vtxos/{address}"
|
||||
@@ -53,6 +48,21 @@ service ArkService {
|
||||
get: "/v1/info"
|
||||
};
|
||||
}
|
||||
rpc Onboard(OnboardRequest) returns (OnboardResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v1/onboard"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
message OnboardRequest {
|
||||
string boarding_tx = 1;
|
||||
Tree congestion_tree = 2;
|
||||
string user_pubkey = 3;
|
||||
}
|
||||
|
||||
message OnboardResponse {
|
||||
}
|
||||
|
||||
message RegisterPaymentRequest {
|
||||
@@ -151,12 +161,6 @@ message PingRequest {
|
||||
|
||||
message PingResponse {}
|
||||
|
||||
message FaucetRequest {
|
||||
string address = 1;
|
||||
}
|
||||
|
||||
message FaucetResponse {}
|
||||
|
||||
message ListVtxosRequest {
|
||||
string address = 1;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -230,58 +230,6 @@ func local_request_ArkService_Ping_0(ctx context.Context, marshaler runtime.Mars
|
||||
|
||||
}
|
||||
|
||||
func request_ArkService_Faucet_0(ctx context.Context, marshaler runtime.Marshaler, client ArkServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq FaucetRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
var (
|
||||
val string
|
||||
ok bool
|
||||
err error
|
||||
_ = err
|
||||
)
|
||||
|
||||
val, ok = pathParams["address"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "address")
|
||||
}
|
||||
|
||||
protoReq.Address, err = runtime.String(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "address", err)
|
||||
}
|
||||
|
||||
msg, err := client.Faucet(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func local_request_ArkService_Faucet_0(ctx context.Context, marshaler runtime.Marshaler, server ArkServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq FaucetRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
var (
|
||||
val string
|
||||
ok bool
|
||||
err error
|
||||
_ = err
|
||||
)
|
||||
|
||||
val, ok = pathParams["address"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "address")
|
||||
}
|
||||
|
||||
protoReq.Address, err = runtime.String(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "address", err)
|
||||
}
|
||||
|
||||
msg, err := server.Faucet(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func request_ArkService_ListVtxos_0(ctx context.Context, marshaler runtime.Marshaler, client ArkServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq ListVtxosRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
@@ -352,6 +300,32 @@ func local_request_ArkService_GetInfo_0(ctx context.Context, marshaler runtime.M
|
||||
|
||||
}
|
||||
|
||||
func request_ArkService_Onboard_0(ctx context.Context, marshaler runtime.Marshaler, client ArkServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq OnboardRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
msg, err := client.Onboard(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func local_request_ArkService_Onboard_0(ctx context.Context, marshaler runtime.Marshaler, server ArkServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq OnboardRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
msg, err := server.Onboard(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
// RegisterArkServiceHandlerServer registers the http handlers for service ArkService to "mux".
|
||||
// UnaryRPC :call ArkServiceServer directly.
|
||||
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
|
||||
@@ -490,31 +464,6 @@ func RegisterArkServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux,
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_ArkService_Faucet_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
var stream runtime.ServerTransportStream
|
||||
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
var err error
|
||||
var annotatedContext context.Context
|
||||
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/ark.v1.ArkService/Faucet", runtime.WithHTTPPathPattern("/v1/faucet/{address}"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_ArkService_Faucet_0(annotatedContext, inboundMarshaler, server, req, pathParams)
|
||||
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_ArkService_Faucet_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("GET", pattern_ArkService_ListVtxos_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
@@ -565,6 +514,31 @@ func RegisterArkServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux,
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_ArkService_Onboard_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
var stream runtime.ServerTransportStream
|
||||
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
var err error
|
||||
var annotatedContext context.Context
|
||||
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/ark.v1.ArkService/Onboard", runtime.WithHTTPPathPattern("/v1/onboard"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_ArkService_Onboard_0(annotatedContext, inboundMarshaler, server, req, pathParams)
|
||||
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_ArkService_Onboard_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -738,28 +712,6 @@ func RegisterArkServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux,
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_ArkService_Faucet_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
var err error
|
||||
var annotatedContext context.Context
|
||||
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/ark.v1.ArkService/Faucet", runtime.WithHTTPPathPattern("/v1/faucet/{address}"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_ArkService_Faucet_0(annotatedContext, inboundMarshaler, client, req, pathParams)
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_ArkService_Faucet_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("GET", pattern_ArkService_ListVtxos_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
@@ -804,6 +756,28 @@ func RegisterArkServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux,
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_ArkService_Onboard_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
var err error
|
||||
var annotatedContext context.Context
|
||||
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/ark.v1.ArkService/Onboard", runtime.WithHTTPPathPattern("/v1/onboard"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_ArkService_Onboard_0(annotatedContext, inboundMarshaler, client, req, pathParams)
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_ArkService_Onboard_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -820,11 +794,11 @@ var (
|
||||
|
||||
pattern_ArkService_Ping_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "ping", "payment_id"}, ""))
|
||||
|
||||
pattern_ArkService_Faucet_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "faucet", "address"}, ""))
|
||||
|
||||
pattern_ArkService_ListVtxos_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "vtxos", "address"}, ""))
|
||||
|
||||
pattern_ArkService_GetInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "info"}, ""))
|
||||
|
||||
pattern_ArkService_Onboard_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "onboard"}, ""))
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -840,9 +814,9 @@ var (
|
||||
|
||||
forward_ArkService_Ping_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_ArkService_Faucet_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_ArkService_ListVtxos_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_ArkService_GetInfo_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_ArkService_Onboard_0 = runtime.ForwardResponseMessage
|
||||
)
|
||||
|
||||
@@ -24,9 +24,9 @@ type ArkServiceClient interface {
|
||||
GetRound(ctx context.Context, in *GetRoundRequest, opts ...grpc.CallOption) (*GetRoundResponse, error)
|
||||
GetEventStream(ctx context.Context, in *GetEventStreamRequest, opts ...grpc.CallOption) (ArkService_GetEventStreamClient, error)
|
||||
Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error)
|
||||
Faucet(ctx context.Context, in *FaucetRequest, opts ...grpc.CallOption) (*FaucetResponse, error)
|
||||
ListVtxos(ctx context.Context, in *ListVtxosRequest, opts ...grpc.CallOption) (*ListVtxosResponse, error)
|
||||
GetInfo(ctx context.Context, in *GetInfoRequest, opts ...grpc.CallOption) (*GetInfoResponse, error)
|
||||
Onboard(ctx context.Context, in *OnboardRequest, opts ...grpc.CallOption) (*OnboardResponse, error)
|
||||
}
|
||||
|
||||
type arkServiceClient struct {
|
||||
@@ -114,15 +114,6 @@ func (c *arkServiceClient) Ping(ctx context.Context, in *PingRequest, opts ...gr
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *arkServiceClient) Faucet(ctx context.Context, in *FaucetRequest, opts ...grpc.CallOption) (*FaucetResponse, error) {
|
||||
out := new(FaucetResponse)
|
||||
err := c.cc.Invoke(ctx, "/ark.v1.ArkService/Faucet", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *arkServiceClient) ListVtxos(ctx context.Context, in *ListVtxosRequest, opts ...grpc.CallOption) (*ListVtxosResponse, error) {
|
||||
out := new(ListVtxosResponse)
|
||||
err := c.cc.Invoke(ctx, "/ark.v1.ArkService/ListVtxos", in, out, opts...)
|
||||
@@ -141,6 +132,15 @@ func (c *arkServiceClient) GetInfo(ctx context.Context, in *GetInfoRequest, opts
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *arkServiceClient) Onboard(ctx context.Context, in *OnboardRequest, opts ...grpc.CallOption) (*OnboardResponse, error) {
|
||||
out := new(OnboardResponse)
|
||||
err := c.cc.Invoke(ctx, "/ark.v1.ArkService/Onboard", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ArkServiceServer is the server API for ArkService service.
|
||||
// All implementations should embed UnimplementedArkServiceServer
|
||||
// for forward compatibility
|
||||
@@ -151,9 +151,9 @@ type ArkServiceServer interface {
|
||||
GetRound(context.Context, *GetRoundRequest) (*GetRoundResponse, error)
|
||||
GetEventStream(*GetEventStreamRequest, ArkService_GetEventStreamServer) error
|
||||
Ping(context.Context, *PingRequest) (*PingResponse, error)
|
||||
Faucet(context.Context, *FaucetRequest) (*FaucetResponse, error)
|
||||
ListVtxos(context.Context, *ListVtxosRequest) (*ListVtxosResponse, error)
|
||||
GetInfo(context.Context, *GetInfoRequest) (*GetInfoResponse, error)
|
||||
Onboard(context.Context, *OnboardRequest) (*OnboardResponse, error)
|
||||
}
|
||||
|
||||
// UnimplementedArkServiceServer should be embedded to have forward compatible implementations.
|
||||
@@ -178,15 +178,15 @@ func (UnimplementedArkServiceServer) GetEventStream(*GetEventStreamRequest, ArkS
|
||||
func (UnimplementedArkServiceServer) Ping(context.Context, *PingRequest) (*PingResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Ping not implemented")
|
||||
}
|
||||
func (UnimplementedArkServiceServer) Faucet(context.Context, *FaucetRequest) (*FaucetResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Faucet not implemented")
|
||||
}
|
||||
func (UnimplementedArkServiceServer) ListVtxos(context.Context, *ListVtxosRequest) (*ListVtxosResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListVtxos not implemented")
|
||||
}
|
||||
func (UnimplementedArkServiceServer) GetInfo(context.Context, *GetInfoRequest) (*GetInfoResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetInfo not implemented")
|
||||
}
|
||||
func (UnimplementedArkServiceServer) Onboard(context.Context, *OnboardRequest) (*OnboardResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Onboard not implemented")
|
||||
}
|
||||
|
||||
// UnsafeArkServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to ArkServiceServer will
|
||||
@@ -310,24 +310,6 @@ func _ArkService_Ping_Handler(srv interface{}, ctx context.Context, dec func(int
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _ArkService_Faucet_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(FaucetRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ArkServiceServer).Faucet(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/ark.v1.ArkService/Faucet",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ArkServiceServer).Faucet(ctx, req.(*FaucetRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _ArkService_ListVtxos_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListVtxosRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@@ -364,6 +346,24 @@ func _ArkService_GetInfo_Handler(srv interface{}, ctx context.Context, dec func(
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _ArkService_Onboard_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(OnboardRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ArkServiceServer).Onboard(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/ark.v1.ArkService/Onboard",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ArkServiceServer).Onboard(ctx, req.(*OnboardRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// ArkService_ServiceDesc is the grpc.ServiceDesc for ArkService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@@ -391,10 +391,6 @@ var ArkService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "Ping",
|
||||
Handler: _ArkService_Ping_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Faucet",
|
||||
Handler: _ArkService_Faucet_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListVtxos",
|
||||
Handler: _ArkService_ListVtxos_Handler,
|
||||
@@ -403,6 +399,10 @@ var ArkService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "GetInfo",
|
||||
Handler: _ArkService_GetInfo_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Onboard",
|
||||
Handler: _ArkService_Onboard_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
oceanwallet "github.com/ark-network/ark/internal/infrastructure/ocean-wallet"
|
||||
scheduler "github.com/ark-network/ark/internal/infrastructure/scheduler/gocron"
|
||||
txbuilder "github.com/ark-network/ark/internal/infrastructure/tx-builder/covenant"
|
||||
txbuilderdummy "github.com/ark-network/ark/internal/infrastructure/tx-builder/dummy"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/vulpemventures/go-elements/network"
|
||||
)
|
||||
@@ -24,7 +23,6 @@ var (
|
||||
"gocron": {},
|
||||
}
|
||||
supportedTxBuilders = supportedType{
|
||||
"dummy": {},
|
||||
"covenant": {},
|
||||
}
|
||||
supportedScanners = supportedType{
|
||||
@@ -167,8 +165,6 @@ func (c *Config) txBuilderService() error {
|
||||
net := c.mainChain()
|
||||
|
||||
switch c.TxBuilderType {
|
||||
case "dummy":
|
||||
svc = txbuilderdummy.NewTxBuilder(c.wallet, net)
|
||||
case "covenant":
|
||||
svc = txbuilder.NewTxBuilder(c.wallet, net, c.RoundLifetime, c.ExitDelay)
|
||||
default:
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/ark-network/ark/internal/core/domain"
|
||||
"github.com/ark-network/ark/internal/core/ports"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
@@ -20,10 +21,6 @@ import (
|
||||
var (
|
||||
paymentsThreshold = int64(128)
|
||||
dustAmount = uint64(450)
|
||||
faucetVtxo = domain.VtxoKey{
|
||||
Txid: "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
VOut: 0,
|
||||
}
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
@@ -32,12 +29,12 @@ type Service interface {
|
||||
SpendVtxos(ctx context.Context, inputs []domain.VtxoKey) (string, error)
|
||||
ClaimVtxos(ctx context.Context, creds string, receivers []domain.Receiver) error
|
||||
SignVtxos(ctx context.Context, forfeitTxs []string) error
|
||||
FaucetVtxos(ctx context.Context, pubkey *secp256k1.PublicKey) error
|
||||
GetRoundByTxid(ctx context.Context, poolTxid string) (*domain.Round, error)
|
||||
GetEventsChannel(ctx context.Context) <-chan domain.RoundEvent
|
||||
UpdatePaymentStatus(ctx context.Context, id string) error
|
||||
ListVtxos(ctx context.Context, pubkey *secp256k1.PublicKey) ([]domain.Vtxo, error)
|
||||
GetInfo(ctx context.Context) (string, int64, int64, error)
|
||||
Onboard(ctx context.Context, boardingTx string, congestionTree tree.CongestionTree, userPubkey *secp256k1.PublicKey) error
|
||||
}
|
||||
|
||||
type service struct {
|
||||
@@ -88,8 +85,9 @@ func NewService(
|
||||
}
|
||||
repoManager.RegisterEventsHandler(
|
||||
func(round *domain.Round) {
|
||||
svc.updateProjectionStore(round)
|
||||
svc.propagateEvents(round)
|
||||
go svc.updateVtxoSet(round)
|
||||
go svc.propagateEvents(round)
|
||||
go svc.scheduleSweepVtxosForRound(round)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -165,43 +163,6 @@ func (s *service) UpdatePaymentStatus(_ context.Context, id string) error {
|
||||
return s.paymentRequests.updatePingTimestamp(id)
|
||||
}
|
||||
|
||||
func (s *service) FaucetVtxos(ctx context.Context, userPubkey *secp256k1.PublicKey) error {
|
||||
pubkey := hex.EncodeToString(userPubkey.SerializeCompressed())
|
||||
|
||||
payment, err := domain.NewPayment([]domain.Vtxo{
|
||||
{
|
||||
VtxoKey: faucetVtxo,
|
||||
Receiver: domain.Receiver{
|
||||
Pubkey: pubkey,
|
||||
Amount: 10000,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := payment.AddReceivers([]domain.Receiver{
|
||||
{Pubkey: pubkey, Amount: 1000},
|
||||
{Pubkey: pubkey, Amount: 1000},
|
||||
{Pubkey: pubkey, Amount: 1000},
|
||||
{Pubkey: pubkey, Amount: 1000},
|
||||
{Pubkey: pubkey, Amount: 1000},
|
||||
{Pubkey: pubkey, Amount: 1000},
|
||||
{Pubkey: pubkey, Amount: 1000},
|
||||
{Pubkey: pubkey, Amount: 1000},
|
||||
{Pubkey: pubkey, Amount: 1000},
|
||||
{Pubkey: pubkey, Amount: 1000},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.paymentRequests.push(*payment); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.paymentRequests.updatePingTimestamp(payment.Id)
|
||||
}
|
||||
|
||||
func (s *service) SignVtxos(ctx context.Context, forfeitTxs []string) error {
|
||||
return s.forfeitTxs.sign(forfeitTxs)
|
||||
}
|
||||
@@ -223,6 +184,40 @@ func (s *service) GetInfo(ctx context.Context) (string, int64, int64, error) {
|
||||
return hex.EncodeToString(s.pubkey.SerializeCompressed()), s.roundLifetime, s.exitDelay, nil
|
||||
}
|
||||
|
||||
func (s *service) Onboard(
|
||||
ctx context.Context, boardingTx string,
|
||||
congestionTree tree.CongestionTree, userPubkey *secp256k1.PublicKey,
|
||||
) error {
|
||||
if err := tree.ValidateCongestionTree(
|
||||
congestionTree, boardingTx, s.pubkey, s.roundLifetime,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
ptx, err := psetv2.NewPsetFromBase64(boardingTx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse boarding tx: %s", err)
|
||||
}
|
||||
|
||||
utx, _ := ptx.UnsignedTx()
|
||||
txid := utx.TxHash().String()
|
||||
|
||||
isConfirmed, _, err := s.wallet.IsTransactionConfirmed(ctx, txid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch confirmation info for boaridng tx: %s", err)
|
||||
}
|
||||
if !isConfirmed {
|
||||
return fmt.Errorf("boarding tx not confirmed yet, please retry later")
|
||||
}
|
||||
|
||||
pubkey := hex.EncodeToString(userPubkey.SerializeCompressed())
|
||||
payments := getPaymentsFromOnboarding(congestionTree, pubkey)
|
||||
round := domain.NewFinalizedRound(
|
||||
dustAmount, pubkey, txid, boardingTx, congestionTree, payments,
|
||||
)
|
||||
|
||||
return s.saveEvents(ctx, round.Id, round.Events())
|
||||
}
|
||||
|
||||
func (s *service) start() {
|
||||
s.startRound()
|
||||
}
|
||||
@@ -365,15 +360,6 @@ func (s *service) finalizeRound() {
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
expirationTimestamp := now + s.roundLifetime + 30 // add 30 secs to be sure that the tx is confirmed
|
||||
|
||||
if err := s.sweeper.schedule(expirationTimestamp, txid, round.CongestionTree); err != nil {
|
||||
changes = round.Fail(fmt.Errorf("failed to schedule sweep tx: %s", err))
|
||||
log.WithError(err).Warn("failed to schedule sweep tx")
|
||||
return
|
||||
}
|
||||
|
||||
changes, _ = round.EndFinalization(forfeitTxs, txid)
|
||||
|
||||
log.Debugf("finalized round %s with pool tx %s", round.Id, round.Txid)
|
||||
@@ -403,49 +389,50 @@ func (s *service) listenToRedemptions() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) updateProjectionStore(round *domain.Round) {
|
||||
ctx := context.Background()
|
||||
lastChange := round.Events()[len(round.Events())-1]
|
||||
func (s *service) updateVtxoSet(round *domain.Round) {
|
||||
// Update the vtxo set only after a round is finalized.
|
||||
if _, ok := lastChange.(domain.RoundFinalized); ok {
|
||||
repo := s.repoManager.Vtxos()
|
||||
spentVtxos := getSpentVtxos(round.Payments)
|
||||
if len(spentVtxos) > 0 {
|
||||
for {
|
||||
if err := repo.SpendVtxos(ctx, spentVtxos); err != nil {
|
||||
log.WithError(err).Warn("failed to add new vtxos, retrying soon")
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
log.Debugf("spent %d vtxos", len(spentVtxos))
|
||||
break
|
||||
}
|
||||
}
|
||||
if !round.IsEnded() {
|
||||
return
|
||||
}
|
||||
|
||||
newVtxos := s.getNewVtxos(round)
|
||||
ctx := context.Background()
|
||||
repo := s.repoManager.Vtxos()
|
||||
spentVtxos := getSpentVtxos(round.Payments)
|
||||
if len(spentVtxos) > 0 {
|
||||
for {
|
||||
if err := repo.AddVtxos(ctx, newVtxos); err != nil {
|
||||
if err := repo.SpendVtxos(ctx, spentVtxos); err != nil {
|
||||
log.WithError(err).Warn("failed to add new vtxos, retrying soon")
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
log.Debugf("added %d new vtxos", len(newVtxos))
|
||||
log.Debugf("spent %d vtxos", len(spentVtxos))
|
||||
break
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
if err := s.startWatchingVtxos(newVtxos); err != nil {
|
||||
log.WithError(err).Warn(
|
||||
"failed to start watching vtxos, retrying in a moment...",
|
||||
)
|
||||
continue
|
||||
}
|
||||
log.Debugf("started watching %d vtxos", len(newVtxos))
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
newVtxos := s.getNewVtxos(round)
|
||||
for {
|
||||
if err := repo.AddVtxos(ctx, newVtxos); err != nil {
|
||||
log.WithError(err).Warn("failed to add new vtxos, retrying soon")
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
log.Debugf("added %d new vtxos", len(newVtxos))
|
||||
break
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
if err := s.startWatchingVtxos(newVtxos); err != nil {
|
||||
log.WithError(err).Warn(
|
||||
"failed to start watching vtxos, retrying in a moment...",
|
||||
)
|
||||
continue
|
||||
}
|
||||
log.Debugf("started watching %d vtxos", len(newVtxos))
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *service) propagateEvents(round *domain.Round) {
|
||||
@@ -465,6 +452,23 @@ func (s *service) propagateEvents(round *domain.Round) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) scheduleSweepVtxosForRound(round *domain.Round) {
|
||||
// Schedule the sweeping procedure only for completed round.
|
||||
if !round.IsEnded() {
|
||||
return
|
||||
}
|
||||
|
||||
expirationTimestamp := time.Now().Add(
|
||||
time.Duration(s.roundLifetime+30) * time.Second,
|
||||
)
|
||||
|
||||
if err := s.sweeper.schedule(
|
||||
expirationTimestamp.Unix(), round.Txid, round.CongestionTree,
|
||||
); err != nil {
|
||||
log.WithError(err).Warn("failed to schedule sweep tx")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) getNewVtxos(round *domain.Round) []domain.Vtxo {
|
||||
leaves := round.CongestionTree.Leaves()
|
||||
vtxos := make([]domain.Vtxo, 0)
|
||||
@@ -590,11 +594,25 @@ func getSpentVtxos(payments map[string]domain.Payment) []domain.VtxoKey {
|
||||
vtxos := make([]domain.VtxoKey, 0)
|
||||
for _, p := range payments {
|
||||
for _, vtxo := range p.Inputs {
|
||||
if vtxo.VtxoKey == faucetVtxo {
|
||||
continue
|
||||
}
|
||||
vtxos = append(vtxos, vtxo.VtxoKey)
|
||||
}
|
||||
}
|
||||
return vtxos
|
||||
}
|
||||
|
||||
func getPaymentsFromOnboarding(
|
||||
congestionTree tree.CongestionTree, userKey string,
|
||||
) []domain.Payment {
|
||||
leaves := congestionTree.Leaves()
|
||||
receivers := make([]domain.Receiver, 0, len(leaves))
|
||||
for _, node := range leaves {
|
||||
ptx, _ := psetv2.NewPsetFromBase64(node.Tx)
|
||||
receiver := domain.Receiver{
|
||||
Pubkey: userKey,
|
||||
Amount: ptx.Outputs[0].Value,
|
||||
}
|
||||
receivers = append(receivers, receiver)
|
||||
}
|
||||
payment := domain.NewPaymentUnsafe(nil, receivers)
|
||||
return []domain.Payment{*payment}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ func (s *sweeper) schedule(
|
||||
|
||||
task := s.createTask(roundTxid, congestionTree)
|
||||
fancyTime := time.Unix(expirationTimestamp, 0).Format("2006-01-02 15:04:05")
|
||||
log.Debugf("scheduled sweep task at %s", fancyTime)
|
||||
log.Debugf("scheduled sweep for round %s at %s", roundTxid, fancyTime)
|
||||
if err := s.scheduler.ScheduleTaskOnce(expirationTimestamp, task); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -290,7 +290,7 @@ func (s *sweeper) findSweepableOutputs(
|
||||
newNodesToCheck := make([]tree.Node, 0)
|
||||
|
||||
for _, node := range nodesToCheck {
|
||||
isPublished, blocktime, err := s.wallet.IsTransactionPublished(ctx, node.Txid)
|
||||
isConfirmed, blocktime, err := s.wallet.IsTransactionConfirmed(ctx, node.Txid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -298,10 +298,10 @@ func (s *sweeper) findSweepableOutputs(
|
||||
var expirationTime int64
|
||||
var sweepInputs []ports.SweepInput
|
||||
|
||||
if !isPublished {
|
||||
if !isConfirmed {
|
||||
if _, ok := blocktimeCache[node.ParentTxid]; !ok {
|
||||
isPublished, blocktime, err := s.wallet.IsTransactionPublished(ctx, node.ParentTxid)
|
||||
if !isPublished || err != nil {
|
||||
isConfirmed, blocktime, err := s.wallet.IsTransactionConfirmed(ctx, node.ParentTxid)
|
||||
if !isConfirmed || err != nil {
|
||||
return nil, fmt.Errorf("tx %s not found", node.Txid)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
@@ -154,23 +152,13 @@ func (m *forfeitTxsMap) push(txs []string) {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
faucetTxID, _ := hex.DecodeString(faucetVtxo.Txid)
|
||||
|
||||
for _, tx := range txs {
|
||||
ptx, _ := psetv2.NewPsetFromBase64(tx)
|
||||
utx, _ := ptx.UnsignedTx()
|
||||
|
||||
txid := utx.TxHash().String()
|
||||
signed := false
|
||||
|
||||
// find the faucet vtxos, and mark them as signed
|
||||
for _, input := range ptx.Inputs {
|
||||
if bytes.Equal(input.PreviousTxid, faucetTxID) {
|
||||
signed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
m.forfeitTxs[utx.TxHash().String()] = &signedTx{tx, signed}
|
||||
m.forfeitTxs[txid] = &signedTx{tx, signed}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,14 @@ func NewPayment(inputs []Vtxo) (*Payment, error) {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func NewPaymentUnsafe(inputs []Vtxo, receivers []Receiver) *Payment {
|
||||
return &Payment{
|
||||
Id: uuid.New().String(),
|
||||
Inputs: inputs,
|
||||
Receivers: receivers,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Payment) AddReceivers(receivers []Receiver) (err error) {
|
||||
if p.Receivers == nil {
|
||||
p.Receivers = make([]Receiver, 0)
|
||||
|
||||
@@ -59,6 +59,39 @@ func NewRound(dustAmount uint64) *Round {
|
||||
}
|
||||
}
|
||||
|
||||
func NewFinalizedRound(
|
||||
dustAmount uint64, userKey, poolTxid, poolTx string,
|
||||
congestionTree tree.CongestionTree, payments []Payment,
|
||||
) *Round {
|
||||
r := NewRound(dustAmount)
|
||||
events := []RoundEvent{
|
||||
RoundStarted{
|
||||
Id: r.Id,
|
||||
Timestamp: time.Now().Unix(),
|
||||
},
|
||||
PaymentsRegistered{
|
||||
Id: r.Id,
|
||||
Payments: payments,
|
||||
},
|
||||
RoundFinalizationStarted{
|
||||
Id: r.Id,
|
||||
CongestionTree: congestionTree,
|
||||
PoolTx: poolTx,
|
||||
},
|
||||
RoundFinalized{
|
||||
Id: r.Id,
|
||||
Txid: poolTxid,
|
||||
Timestamp: time.Now().Unix(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, event := range events {
|
||||
r.raise(event)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func NewRoundFromEvents(events []RoundEvent) *Round {
|
||||
r := &Round{}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ type WalletService interface {
|
||||
SelectUtxos(ctx context.Context, asset string, amount uint64) ([]TxInput, uint64, error)
|
||||
BroadcastTransaction(ctx context.Context, txHex string) (string, error)
|
||||
SignPsetWithKey(ctx context.Context, pset string, inputIndexes []int) (string, error) // inputIndexes == nil means sign all inputs
|
||||
IsTransactionPublished(ctx context.Context, txid string) (isPublished bool, blocktime int64, err error)
|
||||
IsTransactionConfirmed(ctx context.Context, txid string) (isConfirmed bool, blocktime int64, err error)
|
||||
EstimateFees(ctx context.Context, pset string) (uint64, error)
|
||||
Close()
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ func (s *service) BroadcastTransaction(
|
||||
return res.GetTxid(), nil
|
||||
}
|
||||
|
||||
func (s *service) IsTransactionPublished(
|
||||
func (s *service) IsTransactionConfirmed(
|
||||
ctx context.Context, txid string,
|
||||
) (bool, int64, error) {
|
||||
_, blocktime, err := s.GetTransaction(ctx, txid)
|
||||
|
||||
@@ -113,8 +113,8 @@ func (b *txBuilder) BuildPoolTx(
|
||||
// generated in the process and takes the shared utxo outpoint as argument.
|
||||
// This is safe as the memory allocated for `craftCongestionTree` is freed
|
||||
// only after `BuildPoolTx` returns.
|
||||
treeFactoryFn, sharedOutputScript, sharedOutputAmount, err := craftCongestionTree(
|
||||
b.net.AssetID, aspPubkey, payments, minRelayFee, b.roundLifetime, b.exitDelay,
|
||||
treeFactoryFn, sharedOutputScript, sharedOutputAmount, err := tree.CraftCongestionTree(
|
||||
b.net.AssetID, aspPubkey, getOffchainReceivers(payments), minRelayFee, b.roundLifetime, b.exitDelay,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
@@ -97,7 +97,7 @@ func (m *mockedWallet) EstimateFees(ctx context.Context, pset string) (uint64, e
|
||||
return res, args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockedWallet) IsTransactionPublished(ctx context.Context, txid string) (bool, int64, error) {
|
||||
func (m *mockedWallet) IsTransactionConfirmed(ctx context.Context, txid string) (bool, int64, error) {
|
||||
args := m.Called(ctx, txid)
|
||||
|
||||
var res bool
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/ark-network/ark/internal/core/domain"
|
||||
"github.com/ark-network/ark/internal/core/ports"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
@@ -14,7 +15,6 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -62,12 +62,15 @@ func getOnchainReceivers(
|
||||
|
||||
func getOffchainReceivers(
|
||||
payments []domain.Payment,
|
||||
) []domain.Receiver {
|
||||
receivers := make([]domain.Receiver, 0)
|
||||
) []tree.Receiver {
|
||||
receivers := make([]tree.Receiver, 0)
|
||||
for _, payment := range payments {
|
||||
for _, receiver := range payment.Receivers {
|
||||
if !receiver.IsOnchain() {
|
||||
receivers = append(receivers, receiver)
|
||||
receivers = append(receivers, tree.Receiver{
|
||||
Pubkey: receiver.Pubkey,
|
||||
Amount: receiver.Amount,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,35 +136,6 @@ func addInputs(
|
||||
return nil
|
||||
}
|
||||
|
||||
// wrapper of updater methods adding a taproot input to the pset with all the necessary data to spend it via any taproot script
|
||||
func addTaprootInput(
|
||||
updater *psetv2.Updater,
|
||||
input psetv2.InputArgs,
|
||||
internalTaprootKey *secp256k1.PublicKey,
|
||||
taprootTree *taproot.IndexedElementsTapScriptTree,
|
||||
) error {
|
||||
if err := updater.AddInputs([]psetv2.InputArgs{input}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := updater.AddInTapInternalKey(0, schnorr.SerializePubKey(internalTaprootKey)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, proof := range taprootTree.LeafMerkleProofs {
|
||||
controlBlock := proof.ToControlBlock(internalTaprootKey)
|
||||
|
||||
if err := updater.AddInTapLeafScript(0, psetv2.TapLeafScript{
|
||||
TapElementsLeaf: taproot.NewBaseTapElementsLeaf(proof.Script),
|
||||
ControlBlock: controlBlock,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func taprootOutputScript(taprootKey *secp256k1.PublicKey) ([]byte, error) {
|
||||
return txscript.NewScriptBuilder().AddOp(txscript.OP_1).AddData(schnorr.SerializePubKey(taprootKey)).Script()
|
||||
}
|
||||
|
||||
@@ -1,285 +0,0 @@
|
||||
package txbuilder
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/ark-network/ark/internal/core/domain"
|
||||
"github.com/ark-network/ark/internal/core/ports"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/vulpemventures/go-elements/address"
|
||||
"github.com/vulpemventures/go-elements/network"
|
||||
"github.com/vulpemventures/go-elements/payment"
|
||||
"github.com/vulpemventures/go-elements/psetv2"
|
||||
"github.com/vulpemventures/go-elements/transaction"
|
||||
)
|
||||
|
||||
const (
|
||||
connectorAmount = 450
|
||||
sevenDays = 7 * 24 * 60 * 60
|
||||
)
|
||||
|
||||
type txBuilder struct {
|
||||
wallet ports.WalletService
|
||||
net network.Network
|
||||
}
|
||||
|
||||
func NewTxBuilder(
|
||||
wallet ports.WalletService, net network.Network,
|
||||
) ports.TxBuilder {
|
||||
return &txBuilder{wallet, net}
|
||||
}
|
||||
|
||||
// BuildSweepTx implements ports.TxBuilder.
|
||||
func (*txBuilder) BuildSweepTx(wallet ports.WalletService, inputs []ports.SweepInput) (signedSweepTx string, err error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// BuildForfeitTxs implements ports.TxBuilder.
|
||||
func (b *txBuilder) BuildForfeitTxs(
|
||||
aspPubkey *secp256k1.PublicKey, poolTx string, payments []domain.Payment,
|
||||
) (connectors []string, forfeitTxs []string, err error) {
|
||||
poolTxID, err := getTxid(poolTx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
aspScript, err := p2wpkhScript(aspPubkey, b.net)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
numberOfConnectors := countSpentVtxos(payments)
|
||||
|
||||
connectors, err = createConnectors(
|
||||
poolTxID,
|
||||
1,
|
||||
psetv2.OutputArgs{
|
||||
Asset: b.net.AssetID,
|
||||
Amount: connectorAmount,
|
||||
Script: aspScript,
|
||||
},
|
||||
aspScript,
|
||||
numberOfConnectors,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
connectorsAsInputs, err := connectorsToInputArgs(connectors)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
forfeitTxs = make([]string, 0)
|
||||
for _, payment := range payments {
|
||||
for _, vtxo := range payment.Inputs {
|
||||
for _, connector := range connectorsAsInputs {
|
||||
forfeitTx, err := createForfeitTx(
|
||||
connector,
|
||||
psetv2.InputArgs{
|
||||
Txid: vtxo.Txid,
|
||||
TxIndex: vtxo.VOut,
|
||||
},
|
||||
vtxo.Amount,
|
||||
aspScript,
|
||||
b.net,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
forfeitTxs = append(forfeitTxs, forfeitTx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return connectors, forfeitTxs, nil
|
||||
}
|
||||
|
||||
// BuildPoolTx implements ports.TxBuilder.
|
||||
func (b *txBuilder) BuildPoolTx(
|
||||
aspPubkey *secp256k1.PublicKey, payments []domain.Payment, minRelayFee uint64,
|
||||
) (poolTx string, congestionTree tree.CongestionTree, err error) {
|
||||
aspScriptBytes, err := p2wpkhScript(aspPubkey, b.net)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
offchainReceivers, onchainReceivers := receiversFromPayments(payments)
|
||||
sharedOutputAmount := sumReceivers(offchainReceivers)
|
||||
|
||||
numberOfConnectors := countSpentVtxos(payments)
|
||||
connectorOutputAmount := connectorAmount * numberOfConnectors
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
outputs := []psetv2.OutputArgs{
|
||||
{
|
||||
Asset: b.net.AssetID,
|
||||
Amount: sharedOutputAmount,
|
||||
Script: aspScriptBytes,
|
||||
},
|
||||
{
|
||||
Asset: b.net.AssetID,
|
||||
Amount: connectorOutputAmount,
|
||||
Script: aspScriptBytes,
|
||||
},
|
||||
}
|
||||
|
||||
amountToSelect := sharedOutputAmount + connectorOutputAmount
|
||||
|
||||
for _, receiver := range onchainReceivers {
|
||||
amountToSelect += receiver.Amount
|
||||
|
||||
receiverScript, err := address.ToOutputScript(receiver.OnchainAddress)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
outputs = append(outputs, psetv2.OutputArgs{
|
||||
Asset: b.net.AssetID,
|
||||
Amount: receiver.Amount,
|
||||
Script: receiverScript,
|
||||
})
|
||||
}
|
||||
|
||||
utxos, change, err := b.wallet.SelectUtxos(ctx, b.net.AssetID, amountToSelect)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if change > 0 {
|
||||
outputs = append(outputs, psetv2.OutputArgs{
|
||||
Asset: b.net.AssetID,
|
||||
Amount: change,
|
||||
Script: aspScriptBytes,
|
||||
})
|
||||
}
|
||||
|
||||
ptx, err := psetv2.New(toInputArgs(utxos), outputs, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
utx, err := ptx.UnsignedTx()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
congestionTree, err = buildCongestionTree(
|
||||
newOutputScriptFactory(aspPubkey, b.net),
|
||||
b.net,
|
||||
utx.TxHash().String(),
|
||||
offchainReceivers,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
poolTx, err = ptx.ToBase64()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return poolTx, congestionTree, err
|
||||
}
|
||||
|
||||
func (b *txBuilder) GetVtxoScript(userPubkey, _ *secp256k1.PublicKey) ([]byte, error) {
|
||||
p2wpkh := payment.FromPublicKey(userPubkey, &b.net, nil)
|
||||
addr, _ := p2wpkh.WitnessPubKeyHash()
|
||||
return address.ToOutputScript(addr)
|
||||
}
|
||||
|
||||
func (b *txBuilder) GetLeafSweepClosure(
|
||||
node tree.Node, userPubKey *secp256k1.PublicKey,
|
||||
) (*psetv2.TapLeafScript, int64, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func connectorsToInputArgs(connectors []string) ([]psetv2.InputArgs, error) {
|
||||
inputs := make([]psetv2.InputArgs, 0, len(connectors)+1)
|
||||
for i, psetb64 := range connectors {
|
||||
tx, err := psetv2.NewPsetFromBase64(psetb64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
utx, err := tx.UnsignedTx()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
txid := utx.TxHash().String()
|
||||
for j := range tx.Outputs {
|
||||
inputs = append(inputs, psetv2.InputArgs{
|
||||
Txid: txid,
|
||||
TxIndex: uint32(j),
|
||||
})
|
||||
if i != len(connectors)-1 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return inputs, nil
|
||||
}
|
||||
|
||||
func getTxid(txStr string) (string, error) {
|
||||
pset, err := psetv2.NewPsetFromBase64(txStr)
|
||||
if err != nil {
|
||||
tx, err := transaction.NewTxFromHex(txStr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return tx.TxHash().String(), nil
|
||||
}
|
||||
|
||||
utx, err := pset.UnsignedTx()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return utx.TxHash().String(), nil
|
||||
}
|
||||
|
||||
func countSpentVtxos(payments []domain.Payment) uint64 {
|
||||
var sum uint64
|
||||
for _, payment := range payments {
|
||||
sum += uint64(len(payment.Inputs))
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
func receiversFromPayments(
|
||||
payments []domain.Payment,
|
||||
) (offchainReceivers, onchainReceivers []domain.Receiver) {
|
||||
for _, payment := range payments {
|
||||
for _, receiver := range payment.Receivers {
|
||||
if receiver.IsOnchain() {
|
||||
onchainReceivers = append(onchainReceivers, receiver)
|
||||
} else {
|
||||
offchainReceivers = append(offchainReceivers, receiver)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func sumReceivers(receivers []domain.Receiver) uint64 {
|
||||
var sum uint64
|
||||
for _, r := range receivers {
|
||||
sum += r.Amount
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
func toInputArgs(
|
||||
ins []ports.TxInput,
|
||||
) []psetv2.InputArgs {
|
||||
inputs := make([]psetv2.InputArgs, 0, len(ins))
|
||||
for _, in := range ins {
|
||||
inputs = append(inputs, psetv2.InputArgs{
|
||||
Txid: in.GetTxid(),
|
||||
TxIndex: in.GetIndex(),
|
||||
})
|
||||
}
|
||||
return inputs
|
||||
}
|
||||
@@ -1,403 +0,0 @@
|
||||
package txbuilder_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/ark-network/ark/internal/core/domain"
|
||||
"github.com/ark-network/ark/internal/core/ports"
|
||||
txbuilder "github.com/ark-network/ark/internal/infrastructure/tx-builder/dummy"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
secp256k1 "github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/vulpemventures/go-elements/network"
|
||||
"github.com/vulpemventures/go-elements/psetv2"
|
||||
)
|
||||
|
||||
const (
|
||||
testingKey = "0218d5ca8b58797b7dbd65c075dd7ba7784b3f38ab71b1a5a8e3f94ba0257654a6"
|
||||
fakePoolTx = "cHNldP8BAgQCAAAAAQQBAQEFAQMBBgEDAfsEAgAAAAABDiDk7dXxh4KQzgLO8i1ABtaLCe4aPL12GVhN1E9zM1ePLwEPBAAAAAABEAT/////AAEDCOgDAAAAAAAAAQQWABSNnpy01UJqd99eTg2M1IpdKId11gf8BHBzZXQCICWyUQcOKcoZBDzzPM1zJOLdqwPsxK4LXnfE/A5c9slaB/wEcHNldAgEAAAAAAABAwh4BQAAAAAAAAEEFgAUjZ6ctNVCanffXk4NjNSKXSiHddYH/ARwc2V0AiAlslEHDinKGQQ88zzNcyTi3asD7MSuC153xPwOXPbJWgf8BHBzZXQIBAAAAAAAAQMI9AEAAAAAAAABBAAH/ARwc2V0AiAlslEHDinKGQQ88zzNcyTi3asD7MSuC153xPwOXPbJWgf8BHBzZXQIBAAAAAAA"
|
||||
)
|
||||
|
||||
type input struct {
|
||||
txid string
|
||||
vout uint32
|
||||
}
|
||||
|
||||
func (i *input) GetTxid() string {
|
||||
return i.txid
|
||||
}
|
||||
|
||||
func (i *input) GetIndex() uint32 {
|
||||
return i.vout
|
||||
}
|
||||
|
||||
func (i *input) GetScript() string {
|
||||
return "a914ea9f486e82efb3dd83a69fd96e3f0113757da03c87"
|
||||
}
|
||||
|
||||
func (i *input) GetAsset() string {
|
||||
return "5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225"
|
||||
}
|
||||
|
||||
func (i *input) GetValue() uint64 {
|
||||
return 1000
|
||||
}
|
||||
|
||||
type mockedWalletService struct{}
|
||||
|
||||
// BroadcastTransaction implements ports.WalletService.
|
||||
func (*mockedWalletService) BroadcastTransaction(ctx context.Context, txHex string) (string, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// Close implements ports.WalletService.
|
||||
func (*mockedWalletService) Close() {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// DeriveAddresses implements ports.WalletService.
|
||||
func (*mockedWalletService) DeriveAddresses(ctx context.Context, num int) ([]string, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// GetPubkey implements ports.WalletService.
|
||||
func (*mockedWalletService) GetPubkey(ctx context.Context) (*secp256k1.PublicKey, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// SignPset implements ports.WalletService.
|
||||
func (*mockedWalletService) SignPset(ctx context.Context, pset string, extractRawTx bool) (string, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// Status implements ports.WalletService.
|
||||
func (*mockedWalletService) Status(ctx context.Context) (ports.WalletStatus, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func (*mockedWalletService) WatchScripts(ctx context.Context, scripts []string) error {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func (*mockedWalletService) UnwatchScripts(ctx context.Context, scripts []string) error {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func (*mockedWalletService) GetNotificationChannel(ctx context.Context) chan []domain.VtxoKey {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func (*mockedWalletService) SelectUtxos(ctx context.Context, asset string, amount uint64) ([]ports.TxInput, uint64, error) {
|
||||
// random txid
|
||||
bytes := make([]byte, 32)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
fakeInput := input{
|
||||
txid: hex.EncodeToString(bytes),
|
||||
vout: 0,
|
||||
}
|
||||
|
||||
return []ports.TxInput{&fakeInput}, 0, nil
|
||||
}
|
||||
|
||||
func (*mockedWalletService) EstimateFees(ctx context.Context, pset string) (uint64, error) {
|
||||
return 100, nil
|
||||
}
|
||||
|
||||
func (*mockedWalletService) SignPsetWithKey(ctx context.Context, pset string, inputIndex []int) (string, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func (*mockedWalletService) IsTransactionPublished(ctx context.Context, txid string) (bool, int64, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func TestBuildCongestionTree(t *testing.T) {
|
||||
builder := txbuilder.NewTxBuilder(&mockedWalletService{}, network.Liquid)
|
||||
|
||||
fixtures := []struct {
|
||||
payments []domain.Payment
|
||||
expectedNodesNum int // 2*len(receivers)-1
|
||||
expectedLeavesNum int
|
||||
}{
|
||||
{
|
||||
payments: []domain.Payment{
|
||||
{
|
||||
Id: "0",
|
||||
Inputs: []domain.Vtxo{
|
||||
{
|
||||
VtxoKey: domain.VtxoKey{
|
||||
Txid: "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
|
||||
VOut: 0,
|
||||
},
|
||||
Receiver: domain.Receiver{
|
||||
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
Amount: 600,
|
||||
},
|
||||
},
|
||||
},
|
||||
Receivers: []domain.Receiver{
|
||||
{
|
||||
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
Amount: 600,
|
||||
},
|
||||
{
|
||||
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
Amount: 400,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedNodesNum: 3,
|
||||
expectedLeavesNum: 2,
|
||||
},
|
||||
{
|
||||
payments: []domain.Payment{
|
||||
{
|
||||
Id: "0",
|
||||
Inputs: []domain.Vtxo{
|
||||
{
|
||||
VtxoKey: domain.VtxoKey{
|
||||
Txid: "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
|
||||
VOut: 0,
|
||||
},
|
||||
Receiver: domain.Receiver{
|
||||
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
Amount: 600,
|
||||
},
|
||||
},
|
||||
},
|
||||
Receivers: []domain.Receiver{
|
||||
{
|
||||
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
Amount: 600,
|
||||
},
|
||||
{
|
||||
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
Amount: 400,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "0",
|
||||
Inputs: []domain.Vtxo{
|
||||
{
|
||||
VtxoKey: domain.VtxoKey{
|
||||
Txid: "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
|
||||
VOut: 0,
|
||||
},
|
||||
Receiver: domain.Receiver{
|
||||
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
Amount: 600,
|
||||
},
|
||||
},
|
||||
},
|
||||
Receivers: []domain.Receiver{
|
||||
{
|
||||
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
Amount: 600,
|
||||
},
|
||||
{
|
||||
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
Amount: 400,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "0",
|
||||
Inputs: []domain.Vtxo{
|
||||
{
|
||||
VtxoKey: domain.VtxoKey{
|
||||
Txid: "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
|
||||
VOut: 0,
|
||||
},
|
||||
Receiver: domain.Receiver{
|
||||
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
Amount: 600,
|
||||
},
|
||||
},
|
||||
},
|
||||
Receivers: []domain.Receiver{
|
||||
{
|
||||
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
Amount: 600,
|
||||
},
|
||||
{
|
||||
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
Amount: 400,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedNodesNum: 11,
|
||||
expectedLeavesNum: 6,
|
||||
},
|
||||
}
|
||||
pubkeyBytes, _ := hex.DecodeString(testingKey)
|
||||
pubkey, _ := secp256k1.ParsePubKey(pubkeyBytes)
|
||||
|
||||
for _, f := range fixtures {
|
||||
poolTx, tree, err := builder.BuildPoolTx(pubkey, f.payments, 30)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, f.expectedNodesNum, tree.NumberOfNodes())
|
||||
require.Len(t, tree.Leaves(), f.expectedLeavesNum)
|
||||
|
||||
poolPset, err := psetv2.NewPsetFromBase64(poolTx)
|
||||
require.NoError(t, err)
|
||||
|
||||
poolTxUnsigned, err := poolPset.UnsignedTx()
|
||||
require.NoError(t, err)
|
||||
|
||||
poolTxID := poolTxUnsigned.TxHash().String()
|
||||
|
||||
// check the root
|
||||
require.Len(t, tree[0], 1)
|
||||
require.Equal(t, poolTxID, tree[0][0].ParentTxid)
|
||||
|
||||
// check the leaves
|
||||
for _, leaf := range tree.Leaves() {
|
||||
pset, err := psetv2.NewPsetFromBase64(leaf.Tx)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, pset.Inputs, 1)
|
||||
require.Len(t, pset.Outputs, 1)
|
||||
|
||||
inputTxID := chainhash.Hash(pset.Inputs[0].PreviousTxid).String()
|
||||
require.Equal(t, leaf.ParentTxid, inputTxID)
|
||||
}
|
||||
|
||||
// check the nodes
|
||||
for _, level := range tree[:len(tree)-2] {
|
||||
for _, node := range level {
|
||||
pset, err := psetv2.NewPsetFromBase64(node.Tx)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, pset.Inputs, 1)
|
||||
require.Len(t, pset.Outputs, 2)
|
||||
|
||||
inputTxID := chainhash.Hash(pset.Inputs[0].PreviousTxid).String()
|
||||
require.Equal(t, node.ParentTxid, inputTxID)
|
||||
|
||||
children := tree.Children(node.Txid)
|
||||
require.Len(t, children, 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildForfeitTxs(t *testing.T) {
|
||||
builder := txbuilder.NewTxBuilder(&mockedWalletService{}, network.Liquid)
|
||||
|
||||
poolPset, err := psetv2.NewPsetFromBase64(fakePoolTx)
|
||||
require.NoError(t, err)
|
||||
|
||||
poolTxUnsigned, err := poolPset.UnsignedTx()
|
||||
require.NoError(t, err)
|
||||
|
||||
poolTxID := poolTxUnsigned.TxHash().String()
|
||||
|
||||
fixtures := []struct {
|
||||
payments []domain.Payment
|
||||
expectedNumOfForfeitTxs int
|
||||
expectedNumOfConnectors int
|
||||
}{
|
||||
{
|
||||
payments: []domain.Payment{
|
||||
{
|
||||
Id: "0",
|
||||
Inputs: []domain.Vtxo{
|
||||
{
|
||||
VtxoKey: domain.VtxoKey{
|
||||
Txid: "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
|
||||
VOut: 0,
|
||||
},
|
||||
Receiver: domain.Receiver{
|
||||
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
Amount: 600,
|
||||
},
|
||||
},
|
||||
{
|
||||
VtxoKey: domain.VtxoKey{
|
||||
Txid: "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
|
||||
VOut: 1,
|
||||
},
|
||||
Receiver: domain.Receiver{
|
||||
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
Amount: 400,
|
||||
},
|
||||
},
|
||||
},
|
||||
Receivers: []domain.Receiver{
|
||||
{
|
||||
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
Amount: 600,
|
||||
},
|
||||
{
|
||||
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
Amount: 400,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedNumOfForfeitTxs: 4,
|
||||
expectedNumOfConnectors: 1,
|
||||
},
|
||||
}
|
||||
|
||||
pubkeyBytes, _ := hex.DecodeString(testingKey)
|
||||
pubkey, _ := secp256k1.ParsePubKey(pubkeyBytes)
|
||||
|
||||
for _, f := range fixtures {
|
||||
connectors, forfeitTxs, err := builder.BuildForfeitTxs(
|
||||
pubkey, fakePoolTx, f.payments,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, connectors, f.expectedNumOfConnectors)
|
||||
require.Len(t, forfeitTxs, f.expectedNumOfForfeitTxs)
|
||||
|
||||
// decode and check connectors
|
||||
connectorsPsets := make([]*psetv2.Pset, 0, f.expectedNumOfConnectors)
|
||||
for _, pset := range connectors {
|
||||
p, err := psetv2.NewPsetFromBase64(pset)
|
||||
require.NoError(t, err)
|
||||
connectorsPsets = append(connectorsPsets, p)
|
||||
}
|
||||
|
||||
for i, pset := range connectorsPsets {
|
||||
require.Len(t, pset.Inputs, 1)
|
||||
require.Len(t, pset.Outputs, 2)
|
||||
|
||||
expectedInputTxid := poolTxID
|
||||
expectedInputVout := uint32(1)
|
||||
if i > 0 {
|
||||
tx, err := connectorsPsets[i-1].UnsignedTx()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, tx)
|
||||
expectedInputTxid = tx.TxHash().String()
|
||||
}
|
||||
|
||||
inputTxid := chainhash.Hash(pset.Inputs[0].PreviousTxid).String()
|
||||
require.Equal(t, expectedInputTxid, inputTxid)
|
||||
require.Equal(t, expectedInputVout, pset.Inputs[0].PreviousTxIndex)
|
||||
}
|
||||
|
||||
// decode and check forfeit txs
|
||||
forfeitTxsPsets := make([]*psetv2.Pset, 0, f.expectedNumOfForfeitTxs)
|
||||
for _, pset := range forfeitTxs {
|
||||
p, err := psetv2.NewPsetFromBase64(pset)
|
||||
require.NoError(t, err)
|
||||
forfeitTxsPsets = append(forfeitTxsPsets, p)
|
||||
}
|
||||
|
||||
// each forfeit tx should have 2 inputs and 2 outputs
|
||||
for _, pset := range forfeitTxsPsets {
|
||||
require.Len(t, pset.Inputs, 2)
|
||||
require.Len(t, pset.Outputs, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package txbuilder
|
||||
|
||||
import (
|
||||
"github.com/vulpemventures/go-elements/psetv2"
|
||||
)
|
||||
|
||||
func createConnectors(
|
||||
poolTxID string,
|
||||
connectorOutputIndex uint32,
|
||||
connectorOutput psetv2.OutputArgs,
|
||||
changeScript []byte,
|
||||
numberOfConnectors uint64,
|
||||
) (connectorsPsets []string, err error) {
|
||||
previousInput := psetv2.InputArgs{
|
||||
Txid: poolTxID,
|
||||
TxIndex: connectorOutputIndex,
|
||||
}
|
||||
|
||||
if numberOfConnectors == 1 {
|
||||
pset, err := psetv2.New(nil, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
updater, err := psetv2.NewUpdater(pset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = updater.AddInputs([]psetv2.InputArgs{previousInput})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = updater.AddOutputs([]psetv2.OutputArgs{connectorOutput})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
base64, err := pset.ToBase64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []string{base64}, nil
|
||||
}
|
||||
|
||||
// compute the initial amount of the connectors output in pool transaction
|
||||
remainingAmount := connectorAmount * numberOfConnectors
|
||||
|
||||
connectorsPset := make([]string, 0, numberOfConnectors-1)
|
||||
for i := uint64(0); i < numberOfConnectors-1; i++ {
|
||||
// create a new pset
|
||||
pset, err := psetv2.New(nil, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updater, err := psetv2.NewUpdater(pset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = updater.AddInputs([]psetv2.InputArgs{previousInput})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = updater.AddOutputs([]psetv2.OutputArgs{connectorOutput})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
changeAmount := remainingAmount - connectorOutput.Amount
|
||||
if changeAmount > 0 {
|
||||
changeOutput := psetv2.OutputArgs{
|
||||
Asset: connectorOutput.Asset,
|
||||
Amount: changeAmount,
|
||||
Script: changeScript,
|
||||
}
|
||||
err = updater.AddOutputs([]psetv2.OutputArgs{changeOutput})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tx, _ := pset.UnsignedTx()
|
||||
txid := tx.TxHash().String()
|
||||
|
||||
// make the change the next previousInput
|
||||
previousInput = psetv2.InputArgs{
|
||||
Txid: txid,
|
||||
TxIndex: 1,
|
||||
}
|
||||
}
|
||||
|
||||
base64, err := pset.ToBase64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
connectorsPset = append(connectorsPset, base64)
|
||||
}
|
||||
|
||||
return connectorsPset, nil
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package txbuilder
|
||||
|
||||
import (
|
||||
"github.com/vulpemventures/go-elements/network"
|
||||
"github.com/vulpemventures/go-elements/psetv2"
|
||||
)
|
||||
|
||||
func createForfeitTx(
|
||||
connectorInput psetv2.InputArgs,
|
||||
vtxoInput psetv2.InputArgs,
|
||||
vtxoAmount uint64,
|
||||
aspScript []byte,
|
||||
net network.Network,
|
||||
) (forfeitTx string, err error) {
|
||||
pset, err := psetv2.New(nil, nil, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
updater, err := psetv2.NewUpdater(pset)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = updater.AddInputs([]psetv2.InputArgs{connectorInput, vtxoInput})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = updater.AddOutputs([]psetv2.OutputArgs{
|
||||
{
|
||||
Asset: net.AssetID,
|
||||
Amount: vtxoAmount,
|
||||
Script: aspScript,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return pset.ToBase64()
|
||||
}
|
||||
@@ -1,309 +0,0 @@
|
||||
package txbuilder
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/ark-network/ark/internal/core/domain"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/vulpemventures/go-elements/address"
|
||||
"github.com/vulpemventures/go-elements/network"
|
||||
"github.com/vulpemventures/go-elements/payment"
|
||||
"github.com/vulpemventures/go-elements/psetv2"
|
||||
)
|
||||
|
||||
const (
|
||||
sharedOutputIndex = 0
|
||||
)
|
||||
|
||||
type outputScriptFactory func(leaves []domain.Receiver) ([]byte, error)
|
||||
|
||||
func p2wpkhScript(publicKey *secp256k1.PublicKey, net network.Network) ([]byte, error) {
|
||||
payment := payment.FromPublicKey(publicKey, &net, nil)
|
||||
addr, err := payment.WitnessPubKeyHash()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return address.ToOutputScript(addr)
|
||||
}
|
||||
|
||||
// newOtputScriptFactory returns an output script factory func that lock funds using the ASP public key only on all branches psbt. The leaves are instead locked by the leaf public key.
|
||||
func newOutputScriptFactory(aspPublicKey *secp256k1.PublicKey, net network.Network) outputScriptFactory {
|
||||
return func(leaves []domain.Receiver) ([]byte, error) {
|
||||
aspScript, err := p2wpkhScript(aspPublicKey, net)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch len(leaves) {
|
||||
case 0:
|
||||
return nil, nil
|
||||
case 1: // it's a leaf
|
||||
buf, err := hex.DecodeString(leaves[0].Pubkey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, err := secp256k1.ParsePubKey(buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p2wpkhScript(key, net)
|
||||
default: // it's a branch, lock funds with ASP public key
|
||||
return aspScript, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// congestionTree builder iteratively creates a binary tree of Pset from a set of receivers
|
||||
// it also expect createOutputScript func managing the output script creation and the network to use (mainly for L-BTC asset id)
|
||||
func buildCongestionTree(
|
||||
createOutputScript outputScriptFactory,
|
||||
net network.Network,
|
||||
poolTxID string,
|
||||
receivers []domain.Receiver,
|
||||
) (congestionTree tree.CongestionTree, err error) {
|
||||
var nodes []*node
|
||||
|
||||
for _, r := range receivers {
|
||||
nodes = append(nodes, newLeaf(createOutputScript, net, r))
|
||||
}
|
||||
|
||||
for len(nodes) > 1 {
|
||||
nodes, err = createTreeLevel(nodes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
psets, err := nodes[0].psets(psetv2.InputArgs{
|
||||
Txid: poolTxID,
|
||||
TxIndex: sharedOutputIndex,
|
||||
}, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
maxLevel := 0
|
||||
for _, psetWithLevel := range psets {
|
||||
if psetWithLevel.level > maxLevel {
|
||||
maxLevel = psetWithLevel.level
|
||||
}
|
||||
}
|
||||
|
||||
congestionTree = make(tree.CongestionTree, maxLevel+1)
|
||||
|
||||
for _, psetWithLevel := range psets {
|
||||
utx, err := psetWithLevel.pset.UnsignedTx()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
txid := utx.TxHash().String()
|
||||
|
||||
psetB64, err := psetWithLevel.pset.ToBase64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parentTxid := chainhash.Hash(psetWithLevel.pset.Inputs[0].PreviousTxid).String()
|
||||
|
||||
congestionTree[psetWithLevel.level] = append(congestionTree[psetWithLevel.level], tree.Node{
|
||||
Txid: txid,
|
||||
Tx: psetB64,
|
||||
ParentTxid: parentTxid,
|
||||
Leaf: psetWithLevel.leaf,
|
||||
})
|
||||
}
|
||||
|
||||
return congestionTree, nil
|
||||
}
|
||||
|
||||
func createTreeLevel(nodes []*node) ([]*node, error) {
|
||||
if len(nodes)%2 != 0 {
|
||||
last := nodes[len(nodes)-1]
|
||||
pairs, err := createTreeLevel(nodes[:len(nodes)-1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return append(pairs, last), nil
|
||||
}
|
||||
|
||||
pairs := make([]*node, 0, len(nodes)/2)
|
||||
for i := 0; i < len(nodes); i += 2 {
|
||||
pairs = append(pairs, newBranch(nodes[i], nodes[i+1]))
|
||||
}
|
||||
return pairs, nil
|
||||
}
|
||||
|
||||
// internal struct to build a binary tree of Pset
|
||||
type node struct {
|
||||
receivers []domain.Receiver
|
||||
left *node
|
||||
right *node
|
||||
createOutputScript outputScriptFactory
|
||||
network network.Network
|
||||
}
|
||||
|
||||
// create a node from a single receiver
|
||||
func newLeaf(
|
||||
createOutputScript outputScriptFactory,
|
||||
network network.Network,
|
||||
receiver domain.Receiver,
|
||||
) *node {
|
||||
return &node{
|
||||
receivers: []domain.Receiver{receiver},
|
||||
createOutputScript: createOutputScript,
|
||||
network: network,
|
||||
left: nil,
|
||||
right: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// aggregate two nodes into a branch node
|
||||
func newBranch(
|
||||
left *node,
|
||||
right *node,
|
||||
) *node {
|
||||
return &node{
|
||||
receivers: append(left.receivers, right.receivers...),
|
||||
createOutputScript: left.createOutputScript,
|
||||
network: left.network,
|
||||
left: left,
|
||||
right: right,
|
||||
}
|
||||
}
|
||||
|
||||
// is it the final node of the tree
|
||||
func (n *node) isLeaf() bool {
|
||||
return n.left == nil && n.right == nil
|
||||
}
|
||||
|
||||
// compute the output amount of a node
|
||||
func (n *node) amount() uint64 {
|
||||
var amount uint64
|
||||
for _, r := range n.receivers {
|
||||
amount += r.Amount
|
||||
}
|
||||
return amount
|
||||
}
|
||||
|
||||
// compute the output script of a node
|
||||
func (n *node) script() ([]byte, error) {
|
||||
return n.createOutputScript(n.receivers)
|
||||
}
|
||||
|
||||
// use script & amount to create OutputArgs
|
||||
func (n *node) output() (*psetv2.OutputArgs, error) {
|
||||
script, err := n.script()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &psetv2.OutputArgs{
|
||||
Asset: n.network.AssetID,
|
||||
Amount: n.amount(),
|
||||
Script: script,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// create the node Pset from the previous node Pset represented by input arg
|
||||
// if node is a branch, it adds two outputs to the Pset, one for the left branch and one for the right branch
|
||||
// if node is a leaf, it only adds one output to the Pset (the node output)
|
||||
func (n *node) pset(input psetv2.InputArgs) (*psetv2.Pset, error) {
|
||||
pset, err := psetv2.New(nil, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updater, err := psetv2.NewUpdater(pset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = updater.AddInputs([]psetv2.InputArgs{input})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if n.isLeaf() {
|
||||
output, err := n.output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = updater.AddOutputs([]psetv2.OutputArgs{*output})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pset, nil
|
||||
}
|
||||
|
||||
outputLeft, err := n.left.output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
outputRight, err := n.right.output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = updater.AddOutputs([]psetv2.OutputArgs{*outputLeft, *outputRight})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pset, nil
|
||||
}
|
||||
|
||||
type psetWithLevel struct {
|
||||
pset *psetv2.Pset
|
||||
level int
|
||||
leaf bool
|
||||
}
|
||||
|
||||
// create the node pset and all the psets of its children recursively, updating the input arg at each step
|
||||
// the function stops when it reaches a leaf node
|
||||
func (n *node) psets(input psetv2.InputArgs, level int) ([]psetWithLevel, error) {
|
||||
pset, err := n.pset(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nodeResult := []psetWithLevel{
|
||||
{pset, level, n.isLeaf()},
|
||||
}
|
||||
|
||||
if n.isLeaf() {
|
||||
return nodeResult, nil
|
||||
}
|
||||
|
||||
unsignedTx, err := pset.UnsignedTx()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
txID := unsignedTx.TxHash().String()
|
||||
|
||||
psetsLeft, err := n.left.psets(psetv2.InputArgs{
|
||||
Txid: txID,
|
||||
TxIndex: 0,
|
||||
}, level+1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
psetsRight, err := n.right.psets(psetv2.InputArgs{
|
||||
Txid: txID,
|
||||
TxIndex: 1,
|
||||
}, level+1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return append(nodeResult, append(psetsLeft, psetsRight...)...), nil
|
||||
}
|
||||
@@ -40,6 +40,37 @@ func NewHandler(service application.Service) arkv1.ArkServiceServer {
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *handler) Onboard(ctx context.Context, req *arkv1.OnboardRequest) (*arkv1.OnboardResponse, error) {
|
||||
if req.GetUserPubkey() == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "missing user pubkey")
|
||||
}
|
||||
|
||||
pubKey, err := hex.DecodeString(req.GetUserPubkey())
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "invalid user pubkey")
|
||||
}
|
||||
|
||||
decodedPubKey, err := secp256k1.ParsePubKey(pubKey)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "invalid user pubkey")
|
||||
}
|
||||
|
||||
if req.GetBoardingTx() == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "missing boarding tx id")
|
||||
}
|
||||
|
||||
tree, err := toCongestionTree(req.GetCongestionTree())
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
|
||||
if err := h.svc.Onboard(ctx, req.GetBoardingTx(), tree, decodedPubKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &arkv1.OnboardResponse{}, nil
|
||||
}
|
||||
|
||||
func (h *handler) Ping(ctx context.Context, req *arkv1.PingRequest) (*arkv1.PingResponse, error) {
|
||||
if req.GetPaymentId() == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "missing payment id")
|
||||
@@ -96,19 +127,6 @@ func (h *handler) FinalizePayment(ctx context.Context, req *arkv1.FinalizePaymen
|
||||
return &arkv1.FinalizePaymentResponse{}, nil
|
||||
}
|
||||
|
||||
func (h *handler) Faucet(ctx context.Context, req *arkv1.FaucetRequest) (*arkv1.FaucetResponse, error) {
|
||||
_, pubkey, _, err := parseAddress(req.GetAddress())
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
|
||||
if err := h.svc.FaucetVtxos(ctx, pubkey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &arkv1.FaucetResponse{}, nil
|
||||
}
|
||||
|
||||
func (h *handler) GetRound(ctx context.Context, req *arkv1.GetRoundRequest) (*arkv1.GetRoundResponse, error) {
|
||||
if len(req.GetTxid()) <= 0 {
|
||||
return nil, status.Error(codes.InvalidArgument, "missing pool txid")
|
||||
@@ -305,3 +323,32 @@ func castCongestionTree(congestionTree tree.CongestionTree) *arkv1.Tree {
|
||||
Levels: levels,
|
||||
}
|
||||
}
|
||||
|
||||
func toCongestionTree(treeFromProto *arkv1.Tree) (tree.CongestionTree, error) {
|
||||
levels := make(tree.CongestionTree, 0, len(treeFromProto.Levels))
|
||||
|
||||
for _, level := range treeFromProto.Levels {
|
||||
nodes := make([]tree.Node, 0, len(level.Nodes))
|
||||
|
||||
for _, node := range level.Nodes {
|
||||
nodes = append(nodes, tree.Node{
|
||||
Txid: node.Txid,
|
||||
Tx: node.Tx,
|
||||
ParentTxid: node.ParentTxid,
|
||||
Leaf: false,
|
||||
})
|
||||
}
|
||||
|
||||
levels = append(levels, nodes)
|
||||
}
|
||||
|
||||
for j, treeLvl := range levels {
|
||||
for i, node := range treeLvl {
|
||||
if len(levels.Children(node.Txid)) == 0 {
|
||||
levels[j][i].Leaf = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return levels, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user