New address encoding (#356)

* [common] rework address encoding

* new address encoding

* replace offchain address by vtxo output key in DB

* merge migrations files into init one

* fix txbuilder fixtures

* fix transaction events
This commit is contained in:
Louis Singer
2024-10-18 16:50:07 +02:00
committed by GitHub
parent b1c9261f14
commit b536a9e652
58 changed files with 2243 additions and 1896 deletions

View File

@@ -506,6 +506,17 @@
} }
} }
}, },
"v1AsyncPaymentInput": {
"type": "object",
"properties": {
"input": {
"$ref": "#/definitions/v1Input"
},
"forfeitLeafHash": {
"type": "string"
}
}
},
"v1CompletePaymentRequest": { "v1CompletePaymentRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -524,7 +535,7 @@
"type": "array", "type": "array",
"items": { "items": {
"type": "object", "type": "object",
"$ref": "#/definitions/v1Input" "$ref": "#/definitions/v1AsyncPaymentInput"
} }
}, },
"outputs": { "outputs": {
@@ -717,9 +728,6 @@
"type": "string", "type": "string",
"format": "uint64", "format": "uint64",
"description": "Amount to send in satoshis." "description": "Amount to send in satoshis."
},
"descriptor": {
"type": "string"
} }
} }
}, },
@@ -1046,9 +1054,6 @@
"outpoint": { "outpoint": {
"$ref": "#/definitions/v1Outpoint" "$ref": "#/definitions/v1Outpoint"
}, },
"descriptor": {
"type": "string"
},
"spent": { "spent": {
"type": "boolean" "type": "boolean"
}, },
@@ -1074,6 +1079,9 @@
"amount": { "amount": {
"type": "string", "type": "string",
"format": "uint64" "format": "uint64"
},
"pubkey": {
"type": "string"
} }
} }
} }

View File

@@ -187,8 +187,13 @@ message PingResponse {
/* Async Payment API messages */ /* Async Payment API messages */
message AsyncPaymentInput {
Input input = 1;
string forfeit_leaf_hash = 2;
}
message CreatePaymentRequest { message CreatePaymentRequest {
repeated Input inputs = 1; repeated AsyncPaymentInput inputs = 1;
repeated Output outputs = 2; repeated Output outputs = 2;
} }
message CreatePaymentResponse { message CreatePaymentResponse {
@@ -290,7 +295,6 @@ message Input {
message Output { message Output {
string address = 1; // onchain or off-chain string address = 1; // onchain or off-chain
uint64 amount = 2; // Amount to send in satoshis. uint64 amount = 2; // Amount to send in satoshis.
string descriptor = 3;
} }
message Tree { message Tree {
@@ -309,18 +313,17 @@ message Node {
message Vtxo { message Vtxo {
Outpoint outpoint = 1; Outpoint outpoint = 1;
string descriptor = 2; bool spent = 2;
bool spent = 3; string round_txid = 3;
string round_txid = 4; string spent_by = 4;
string spent_by = 5; int64 expire_at = 5;
int64 expire_at = 6; bool swept = 6;
bool swept = 7; bool pending = 7;
bool pending = 8; string redeem_tx = 8;
string redeem_tx = 9; uint64 amount = 9;
uint64 amount = 10; string pubkey = 10;
} }
message GetTransactionsStreamRequest {} message GetTransactionsStreamRequest {}
message GetTransactionsStreamResponse { message GetTransactionsStreamResponse {
oneof tx { oneof tx {

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,10 @@
package bitcointree package bitcointree
import ( import (
"encoding/hex"
"fmt" "fmt"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/common/tree"
"github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
@@ -15,7 +17,7 @@ import (
// CraftSharedOutput returns the taproot script and the amount of the initial root output // CraftSharedOutput returns the taproot script and the amount of the initial root output
func CraftSharedOutput( func CraftSharedOutput(
cosigners []*secp256k1.PublicKey, aspPubkey *secp256k1.PublicKey, receivers []Receiver, cosigners []*secp256k1.PublicKey, aspPubkey *secp256k1.PublicKey, receivers []tree.VtxoLeaf,
feeSatsPerNode uint64, roundLifetime int64, feeSatsPerNode uint64, roundLifetime int64,
) ([]byte, int64, error) { ) ([]byte, int64, error) {
aggregatedKey, _, err := createAggregatedKeyWithSweep( aggregatedKey, _, err := createAggregatedKeyWithSweep(
@@ -32,7 +34,7 @@ func CraftSharedOutput(
amount := root.getAmount() + int64(feeSatsPerNode) amount := root.getAmount() + int64(feeSatsPerNode)
scriptPubKey, err := taprootOutputScript(aggregatedKey.FinalKey) scriptPubKey, err := common.P2TRScript(aggregatedKey.FinalKey)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
@@ -42,7 +44,7 @@ func CraftSharedOutput(
// CraftCongestionTree creates all the tree's transactions // CraftCongestionTree creates all the tree's transactions
func CraftCongestionTree( func CraftCongestionTree(
initialInput *wire.OutPoint, cosigners []*secp256k1.PublicKey, aspPubkey *secp256k1.PublicKey, receivers []Receiver, initialInput *wire.OutPoint, cosigners []*secp256k1.PublicKey, aspPubkey *secp256k1.PublicKey, receivers []tree.VtxoLeaf,
feeSatsPerNode uint64, roundLifetime int64, feeSatsPerNode uint64, roundLifetime int64,
) (tree.CongestionTree, error) { ) (tree.CongestionTree, error) {
aggregatedKey, sweepTapLeaf, err := createAggregatedKeyWithSweep( aggregatedKey, sweepTapLeaf, err := createAggregatedKeyWithSweep(
@@ -108,8 +110,8 @@ type node interface {
} }
type leaf struct { type leaf struct {
vtxoScript VtxoScript amount int64
amount int64 pubkey *secp256k1.PublicKey
} }
type branch struct { type branch struct {
@@ -142,12 +144,7 @@ func (l *leaf) getAmount() int64 {
} }
func (l *leaf) getOutputs() ([]*wire.TxOut, error) { func (l *leaf) getOutputs() ([]*wire.TxOut, error) {
taprootKey, _, err := l.vtxoScript.TapTree() script, err := common.P2TRScript(l.pubkey)
if err != nil {
return nil, err
}
script, err := taprootOutputScript(taprootKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -161,7 +158,7 @@ func (l *leaf) getOutputs() ([]*wire.TxOut, error) {
} }
func (b *branch) getOutputs() ([]*wire.TxOut, error) { func (b *branch) getOutputs() ([]*wire.TxOut, error) {
sharedOutputScript, err := taprootOutputScript(b.aggregatedKey.FinalKey) sharedOutputScript, err := common.P2TRScript(b.aggregatedKey.FinalKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -246,7 +243,7 @@ func getTx(
func createRootNode( func createRootNode(
aggregatedKey *musig2.AggregateKey, aggregatedKey *musig2.AggregateKey,
cosigners []*secp256k1.PublicKey, cosigners []*secp256k1.PublicKey,
receivers []Receiver, receivers []tree.VtxoLeaf,
feeSatsPerNode uint64, feeSatsPerNode uint64,
) (root node, err error) { ) (root node, err error) {
if len(receivers) == 0 { if len(receivers) == 0 {
@@ -255,9 +252,19 @@ func createRootNode(
nodes := make([]node, 0, len(receivers)) nodes := make([]node, 0, len(receivers))
for _, r := range receivers { for _, r := range receivers {
pubkeyBytes, err := hex.DecodeString(r.Pubkey)
if err != nil {
return nil, err
}
pubkey, err := schnorr.ParsePubKey(pubkeyBytes)
if err != nil {
return nil, err
}
leafNode := &leaf{ leafNode := &leaf{
vtxoScript: r.Script, amount: int64(r.Amount),
amount: int64(r.Amount), pubkey: pubkey,
} }
nodes = append(nodes, leafNode) nodes = append(nodes, leafNode)
} }
@@ -339,9 +346,3 @@ func createUpperLevel(nodes []node, aggregatedKey *musig2.AggregateKey, cosigner
} }
return pairs, nil return pairs, nil
} }
func taprootOutputScript(taprootKey *secp256k1.PublicKey) ([]byte, error) {
return txscript.NewScriptBuilder().AddOp(txscript.OP_1).AddData(
schnorr.SerializePubKey(taprootKey),
).Script()
}

View File

@@ -7,6 +7,7 @@ import (
"io" "io"
"strings" "strings"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/common/tree"
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr"
@@ -512,7 +513,7 @@ func prevOutFetcherFactory(
func(partial *psbt.Packet) (txscript.PrevOutputFetcher, error), func(partial *psbt.Packet) (txscript.PrevOutputFetcher, error),
error, error,
) { ) {
pkscript, err := taprootOutputScript(finalAggregatedKey) pkscript, err := common.P2TRScript(finalAggregatedKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -2,13 +2,13 @@ package bitcointree_test
import ( import (
"bytes" "bytes"
"encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"testing" "testing"
"github.com/ark-network/ark/common/bitcointree" "github.com/ark-network/ark/common/bitcointree"
"github.com/ark-network/ark/common/tree"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
@@ -44,7 +44,7 @@ func TestRoundTripSignTree(t *testing.T) {
_, sharedOutputAmount, err := bitcointree.CraftSharedOutput( _, sharedOutputAmount, err := bitcointree.CraftSharedOutput(
cosigners, cosigners,
asp.PubKey(), asp.PubKey(),
castReceivers(f.Receivers, asp.PubKey()), castReceivers(f.Receivers),
minRelayFee, minRelayFee,
lifetime, lifetime,
) )
@@ -58,7 +58,7 @@ func TestRoundTripSignTree(t *testing.T) {
}, },
cosigners, cosigners,
asp.PubKey(), asp.PubKey(),
castReceivers(f.Receivers, asp.PubKey()), castReceivers(f.Receivers),
minRelayFee, minRelayFee,
lifetime, lifetime,
) )
@@ -222,29 +222,11 @@ type receiverFixture struct {
Pubkey string `json:"pubkey"` Pubkey string `json:"pubkey"`
} }
func (r receiverFixture) toVtxoScript(asp *secp256k1.PublicKey) bitcointree.VtxoScript { func castReceivers(receivers []receiverFixture) []tree.VtxoLeaf {
bytesKey, err := hex.DecodeString(r.Pubkey) receiversOut := make([]tree.VtxoLeaf, 0, len(receivers))
if err != nil {
panic(err)
}
pubkey, err := secp256k1.ParsePubKey(bytesKey)
if err != nil {
panic(err)
}
return &bitcointree.DefaultVtxoScript{
Owner: pubkey,
Asp: asp,
ExitDelay: exitDelay,
}
}
func castReceivers(receivers []receiverFixture, asp *secp256k1.PublicKey) []bitcointree.Receiver {
receiversOut := make([]bitcointree.Receiver, 0, len(receivers))
for _, r := range receivers { for _, r := range receivers {
receiversOut = append(receiversOut, bitcointree.Receiver{ receiversOut = append(receiversOut, tree.VtxoLeaf{
Script: r.toVtxoScript(asp), Pubkey: r.Pubkey,
Amount: uint64(r.Amount), Amount: uint64(r.Amount),
}) })
} }

View File

@@ -4,7 +4,7 @@
{ {
"receivers": [ "receivers": [
{ {
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "pubkey": "0000000000000000000000000000000000000000000000000000000000000002",
"amount": 1100 "amount": 1100
} }
] ]
@@ -12,11 +12,11 @@
{ {
"receivers": [ "receivers": [
{ {
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "pubkey": "0000000000000000000000000000000000000000000000000000000000000002",
"amount": 1100 "amount": 1100
}, },
{ {
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "pubkey": "0000000000000000000000000000000000000000000000000000000000000002",
"amount": 8000 "amount": 8000
} }
] ]
@@ -24,23 +24,23 @@
{ {
"receivers": [ "receivers": [
{ {
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "pubkey": "0000000000000000000000000000000000000000000000000000000000000002",
"amount": 1100 "amount": 1100
}, },
{ {
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "pubkey": "0000000000000000000000000000000000000000000000000000000000000002",
"amount": 1100 "amount": 1100
}, },
{ {
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "pubkey": "0000000000000000000000000000000000000000000000000000000000000002",
"amount": 1100 "amount": 1100
}, },
{ {
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "pubkey": "0000000000000000000000000000000000000000000000000000000000000002",
"amount": 1000 "amount": 1000
}, },
{ {
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "pubkey": "0000000000000000000000000000000000000000000000000000000000000002",
"amount": 1100 "amount": 1100
} }
] ]

View File

@@ -1,6 +0,0 @@
package bitcointree
type Receiver struct {
Script VtxoScript
Amount uint64
}

View File

@@ -3,63 +3,68 @@ package common
import ( import (
"fmt" "fmt"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil/bech32" "github.com/btcsuite/btcd/btcutil/bech32"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
) )
func EncodeAddress( // Address represents an Ark address with HRP, ASP public key, and VTXO Taproot public key
hrp string, userKey, aspKey *secp256k1.PublicKey, type Address struct {
) (addr string, err error) { HRP string
if userKey == nil { Asp *secp256k1.PublicKey
err = fmt.Errorf("missing public key") VtxoTapKey *secp256k1.PublicKey
return }
// Encode converts the address to its bech32m string representation
func (a *Address) Encode() (string, error) {
if a.Asp == nil {
return "", fmt.Errorf("missing asp public key")
} }
if aspKey == nil { if a.VtxoTapKey == nil {
err = fmt.Errorf("missing asp public key") return "", fmt.Errorf("missing vtxo tap public key")
return
}
if hrp != Liquid.Addr && hrp != LiquidTestNet.Addr {
err = fmt.Errorf("invalid prefix")
return
} }
combinedKey := append( combinedKey := append(
aspKey.SerializeCompressed(), userKey.SerializeCompressed()..., schnorr.SerializePubKey(a.Asp), schnorr.SerializePubKey(a.VtxoTapKey)...,
) )
grp, err := bech32.ConvertBits(combinedKey, 8, 5, true) grp, err := bech32.ConvertBits(combinedKey, 8, 5, true)
if err != nil { if err != nil {
return return "", err
} }
addr, err = bech32.EncodeM(hrp, grp) return bech32.EncodeM(a.HRP, grp)
return
} }
func DecodeAddress( // DecodeAddress parses a bech32m encoded address string and returns an Address object
addr string, func DecodeAddress(addr string) (*Address, error) {
) (hrp string, userKey, aspKey *secp256k1.PublicKey, err error) { if len(addr) == 0 {
return nil, fmt.Errorf("address is empty")
}
prefix, buf, err := bech32.DecodeNoLimit(addr) prefix, buf, err := bech32.DecodeNoLimit(addr)
if err != nil { if err != nil {
return return nil, err
} }
if prefix != Liquid.Addr && prefix != LiquidTestNet.Addr && prefix != LiquidRegTest.Addr { if prefix != Liquid.Addr && prefix != LiquidTestNet.Addr && prefix != LiquidRegTest.Addr {
err = fmt.Errorf("invalid prefix") return nil, fmt.Errorf("invalid prefix")
return
} }
grp, err := bech32.ConvertBits(buf, 5, 8, false) grp, err := bech32.ConvertBits(buf, 5, 8, false)
if err != nil { if err != nil {
return return nil, err
} }
aKey, err := secp256k1.ParsePubKey(grp[:33])
aKey, err := schnorr.ParsePubKey(grp[:32])
if err != nil { if err != nil {
err = fmt.Errorf("failed to parse public key: %s", err) return nil, fmt.Errorf("failed to parse public key: %s", err)
return
} }
uKey, err := secp256k1.ParsePubKey(grp[33:])
vtxoKey, err := schnorr.ParsePubKey(grp[32:])
if err != nil { if err != nil {
err = fmt.Errorf("failed to parse asp public key: %s", err) return nil, fmt.Errorf("failed to parse asp public key: %s", err)
return
} }
hrp = prefix
userKey = uKey return &Address{
aspKey = aKey HRP: prefix,
return Asp: aKey,
VtxoTapKey: vtxoKey,
}, nil
} }

View File

@@ -40,31 +40,29 @@ func TestAddressEncoding(t *testing.T) {
t.Run("valid", func(t *testing.T) { t.Run("valid", func(t *testing.T) {
for _, f := range fixtures.Address.Valid { for _, f := range fixtures.Address.Valid {
hrp, userKey, aspKey, err := common.DecodeAddress(f.Addr) addr, err := common.DecodeAddress(f.Addr)
require.NoError(t, err) require.NoError(t, err)
require.NotEmpty(t, hrp) require.NotEmpty(t, addr.HRP)
require.NotNil(t, userKey) require.NotNil(t, addr.Asp)
require.NotNil(t, aspKey) require.NotNil(t, addr.VtxoTapKey)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, f.ExpectedUserKey, hex.EncodeToString(userKey.SerializeCompressed())) require.Equal(t, f.ExpectedUserKey, hex.EncodeToString(addr.VtxoTapKey.SerializeCompressed()))
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, f.ExpectedAspKey, hex.EncodeToString(aspKey.SerializeCompressed())) require.Equal(t, f.ExpectedAspKey, hex.EncodeToString(addr.Asp.SerializeCompressed()))
addr, err := common.EncodeAddress(hrp, userKey, aspKey) encoded, err := addr.Encode()
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, f.Addr, addr) require.Equal(t, f.Addr, encoded)
} }
}) })
t.Run("invalid", func(t *testing.T) { t.Run("invalid", func(t *testing.T) {
for _, f := range fixtures.Address.Invalid { for _, f := range fixtures.Address.Invalid {
hrp, userKey, aspKey, err := common.DecodeAddress(f.Addr) addr, err := common.DecodeAddress(f.Addr)
require.EqualError(t, err, f.ExpectedError) require.EqualError(t, err, f.ExpectedError)
require.Empty(t, hrp) require.Nil(t, addr)
require.Nil(t, userKey)
require.Nil(t, aspKey)
} }
}) })
} }

View File

@@ -2,9 +2,9 @@
"address": { "address": {
"valid": [ "valid": [
{ {
"addr": "ark1qgvdtj5ttpuhkldavhq8thtm5auyk0ec4dcmrfdgu0u5hgp9we22vqa7mdkrrulzu48law4zzvzz8k59hul0ayl2urt905we5wf6gee68sfrfj35", "addr": "tark1x0lm8hhr2wc6n6lyemtyh9rz8rg2ftpkfun46aca56kjg3ws0tsztfpuanaquxc6faedvjk3tax0575y6perapg3e95654pk8r4fjecs5fyd2",
"expectedUserKey": "03bedb6c31f3e2e54ffebaa2130423da85bf3efe93eae0d657d1d9a393a4673a3c", "expectedUserKey": "0225a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"expectedAspKey": "0218d5ca8b58797b7dbd65c075dd7ba7784b3f38ab71b1a5a8e3f94ba0257654a6" "expectedAspKey": "0233ffb3dee353b1a9ebe4ced64b946238d0a4ac364f275d771da6ad2445d07ae0"
} }
], ],
"invalid": [ "invalid": [

View File

@@ -1,9 +1,9 @@
package tree package tree
import ( import (
"encoding/hex"
"fmt" "fmt"
"github.com/ark-network/ark/common"
"github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
@@ -13,7 +13,7 @@ import (
) )
func CraftCongestionTree( func CraftCongestionTree(
asset string, aspPubkey *secp256k1.PublicKey, receivers []Receiver, asset string, aspPubkey *secp256k1.PublicKey, receivers []VtxoLeaf,
feeSatsPerNode uint64, roundLifetime int64, feeSatsPerNode uint64, roundLifetime int64,
) ( ) (
buildCongestionTree TreeFactory, buildCongestionTree TreeFactory,
@@ -41,9 +41,14 @@ func CraftCongestionTree(
return return
} }
type vtxoOutput struct {
pubkey *secp256k1.PublicKey
amount uint64
}
type node struct { type node struct {
sweepKey *secp256k1.PublicKey sweepKey *secp256k1.PublicKey
receivers []Receiver receivers []vtxoOutput
left *node left *node
right *node right *node
asset string asset string
@@ -61,7 +66,7 @@ func (n *node) isLeaf() bool {
func (n *node) getAmount() uint64 { func (n *node) getAmount() uint64 {
var amount uint64 var amount uint64
for _, r := range n.receivers { for _, r := range n.receivers {
amount += r.Amount amount += r.amount
} }
if n.isLeaf() { if n.isLeaf() {
@@ -107,7 +112,7 @@ func (n *node) getChildren() []*node {
func (n *node) getOutputs() ([]psetv2.OutputArgs, error) { func (n *node) getOutputs() ([]psetv2.OutputArgs, error) {
if n.isLeaf() { if n.isLeaf() {
taprootKey, _, err := n.getVtxoWitnessData() taprootKey, err := n.getVtxoWitnessData()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -168,7 +173,7 @@ func (n *node) getWitnessData() (
} }
if n.isLeaf() { if n.isLeaf() {
taprootKey, _, err := n.getVtxoWitnessData() taprootKey, err := n.getVtxoWitnessData()
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -241,15 +246,14 @@ func (n *node) getWitnessData() (
} }
func (n *node) getVtxoWitnessData() ( func (n *node) getVtxoWitnessData() (
*secp256k1.PublicKey, common.TaprootTree, error, *secp256k1.PublicKey, error,
) { ) {
if !n.isLeaf() { if !n.isLeaf() {
return nil, nil, fmt.Errorf("cannot call vtxoWitness on a non-leaf node") return nil, fmt.Errorf("cannot call vtxoWitness on a non-leaf node")
} }
receiver := n.receivers[0] receiver := n.receivers[0]
return receiver.pubkey, nil
return receiver.Script.TapTree()
} }
func (n *node) getTreeNode( func (n *node) getTreeNode(
@@ -373,7 +377,7 @@ func (n *node) createFinalCongestionTree() TreeFactory {
} }
func createPartialCongestionTree( func createPartialCongestionTree(
asset string, aspPubkey *secp256k1.PublicKey, receivers []Receiver, asset string, aspPubkey *secp256k1.PublicKey, receivers []VtxoLeaf,
feeSatsPerNode uint64, roundLifetime int64, feeSatsPerNode uint64, roundLifetime int64,
) (root *node, err error) { ) (root *node, err error) {
if len(receivers) == 0 { if len(receivers) == 0 {
@@ -382,9 +386,19 @@ func createPartialCongestionTree(
nodes := make([]*node, 0, len(receivers)) nodes := make([]*node, 0, len(receivers))
for _, r := range receivers { for _, r := range receivers {
pubkeyBytes, err := hex.DecodeString(r.Pubkey)
if err != nil {
return nil, err
}
pubkey, err := schnorr.ParsePubKey(pubkeyBytes)
if err != nil {
return nil, err
}
leafNode := &node{ leafNode := &node{
sweepKey: aspPubkey, sweepKey: aspPubkey,
receivers: []Receiver{r}, receivers: []vtxoOutput{{pubkey, r.Amount}},
asset: asset, asset: asset,
feeSats: feeSatsPerNode, feeSats: feeSatsPerNode,
roundLifetime: roundLifetime, roundLifetime: roundLifetime,

View File

@@ -6,7 +6,7 @@ import (
type TreeFactory func(outpoint psetv2.InputArgs) (CongestionTree, error) type TreeFactory func(outpoint psetv2.InputArgs) (CongestionTree, error)
type Receiver struct { type VtxoLeaf struct {
Script VtxoScript Pubkey string
Amount uint64 Amount uint64
} }

View File

@@ -94,7 +94,7 @@ func (a *arkClient) Receive(ctx context.Context) (string, string, error) {
return "", "", err return "", "", err
} }
return offchainAddr, boardingAddr, nil return offchainAddr.Address, boardingAddr.Address, nil
} }
func (a *arkClient) GetTransactionEventChannel() chan types.TransactionEvent { func (a *arkClient) GetTransactionEventChannel() chan types.TransactionEvent {

View File

@@ -2,10 +2,14 @@ package client
import ( import (
"context" "context"
"encoding/hex"
"time" "time"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/bitcointree" "github.com/ark-network/ark/common/bitcointree"
"github.com/ark-network/ark/common/tree" "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/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chainfee"
) )
@@ -41,7 +45,7 @@ type ASPClient interface {
) (<-chan RoundEventChannel, func(), error) ) (<-chan RoundEventChannel, func(), error)
Ping(ctx context.Context, paymentID string) (RoundEvent, error) Ping(ctx context.Context, paymentID string) (RoundEvent, error)
CreatePayment( CreatePayment(
ctx context.Context, inputs []Input, outputs []Output, ctx context.Context, inputs []AsyncPaymentInput, outputs []Output,
) (string, error) ) (string, error)
CompletePayment( CompletePayment(
ctx context.Context, signedRedeemTx string, ctx context.Context, signedRedeemTx string,
@@ -79,21 +83,50 @@ type Input struct {
Descriptor string Descriptor string
} }
type AsyncPaymentInput struct {
Input
ForfeitLeafHash chainhash.Hash
}
type Vtxo struct { type Vtxo struct {
Outpoint Outpoint
Pubkey string
Amount uint64
RoundTxid string
ExpiresAt *time.Time
RedeemTx string
Pending bool
SpentBy string
}
func (v Vtxo) Address(asp *secp256k1.PublicKey, net common.Network) (string, error) {
pubkeyBytes, err := hex.DecodeString(v.Pubkey)
if err != nil {
return "", err
}
pubkey, err := schnorr.ParsePubKey(pubkeyBytes)
if err != nil {
return "", err
}
a := &common.Address{
HRP: net.Addr,
Asp: asp,
VtxoTapKey: pubkey,
}
return a.Encode()
}
type DescriptorVtxo struct {
Vtxo
Descriptor string Descriptor string
Amount uint64
RoundTxid string
ExpiresAt *time.Time
RedeemTx string
Pending bool
SpentBy string
} }
type Output struct { type Output struct {
Address string // onchain output address Address string // onchain or offchain address
Descriptor string // offchain vtxo descriptor Amount uint64
Amount uint64
} }
type RoundStage int type RoundStage int

View File

@@ -238,10 +238,10 @@ func (a *grpcClient) Ping(
} }
func (a *grpcClient) CreatePayment( func (a *grpcClient) CreatePayment(
ctx context.Context, inputs []client.Input, outputs []client.Output, ctx context.Context, inputs []client.AsyncPaymentInput, outputs []client.Output,
) (string, error) { ) (string, error) {
req := &arkv1.CreatePaymentRequest{ req := &arkv1.CreatePaymentRequest{
Inputs: ins(inputs).toProto(), Inputs: asyncIns(inputs).toProto(),
Outputs: outs(outputs).toProto(), Outputs: outs(outputs).toProto(),
} }
resp, err := a.svc.CreatePayment(ctx, req) resp, err := a.svc.CreatePayment(ctx, req)
@@ -404,13 +404,13 @@ func vtxosFromProto(protoVtxos []*arkv1.Vtxo) []client.Vtxo {
Txid: v.Outpoint.Txid, Txid: v.Outpoint.Txid,
VOut: v.Outpoint.Vout, VOut: v.Outpoint.Vout,
}, },
Descriptor: v.Descriptor_, Pubkey: v.Pubkey,
Amount: v.Amount, Amount: v.Amount,
RoundTxid: v.RoundTxid, RoundTxid: v.RoundTxid,
ExpiresAt: &expiresAt, ExpiresAt: &expiresAt,
RedeemTx: v.RedeemTx, RedeemTx: v.RedeemTx,
Pending: v.Pending, Pending: v.Pending,
SpentBy: v.SpentBy, SpentBy: v.SpentBy,
} }
} }
return vtxos return vtxos

View File

@@ -18,9 +18,8 @@ type out client.Output
func (o out) toProto() *arkv1.Output { func (o out) toProto() *arkv1.Output {
return &arkv1.Output{ return &arkv1.Output{
Address: o.Address, Address: o.Address,
Descriptor_: o.Descriptor, Amount: o.Amount,
Amount: o.Amount,
} }
} }
@@ -123,13 +122,13 @@ func (v vtxo) toVtxo() client.Vtxo {
Txid: v.GetOutpoint().GetTxid(), Txid: v.GetOutpoint().GetTxid(),
VOut: v.GetOutpoint().GetVout(), VOut: v.GetOutpoint().GetVout(),
}, },
Amount: v.GetAmount(), Amount: v.GetAmount(),
RoundTxid: v.GetRoundTxid(), RoundTxid: v.GetRoundTxid(),
ExpiresAt: expiresAt, ExpiresAt: expiresAt,
Pending: v.GetPending(), Pending: v.GetPending(),
RedeemTx: v.GetRedeemTx(), RedeemTx: v.GetRedeemTx(),
SpentBy: v.GetSpentBy(), SpentBy: v.GetSpentBy(),
Descriptor: v.GetDescriptor_(), Pubkey: v.GetPubkey(),
} }
} }
@@ -153,6 +152,23 @@ func toProtoInput(i client.Input) *arkv1.Input {
} }
} }
func toAsyncProtoInput(i client.AsyncPaymentInput) *arkv1.AsyncPaymentInput {
return &arkv1.AsyncPaymentInput{
Input: toProtoInput(i.Input),
ForfeitLeafHash: i.ForfeitLeafHash.String(),
}
}
type asyncIns []client.AsyncPaymentInput
func (i asyncIns) toProto() []*arkv1.AsyncPaymentInput {
list := make([]*arkv1.AsyncPaymentInput, 0, len(i))
for _, ii := range i {
list = append(list, toAsyncProtoInput(ii))
}
return list
}
type ins []client.Input type ins []client.Input
func (i ins) toProto() []*arkv1.Input { func (i ins) toProto() []*arkv1.Input {

View File

@@ -138,9 +138,8 @@ func (a *restClient) RegisterOutputsForNextRound(
outs := make([]*models.V1Output, 0, len(outputs)) outs := make([]*models.V1Output, 0, len(outputs))
for _, o := range outputs { for _, o := range outputs {
outs = append(outs, &models.V1Output{ outs = append(outs, &models.V1Output{
Address: o.Address, Address: o.Address,
Descriptor: o.Descriptor, Amount: strconv.Itoa(int(o.Amount)),
Amount: strconv.Itoa(int(o.Amount)),
}) })
} }
body := models.V1RegisterOutputsForNextRoundRequest{ body := models.V1RegisterOutputsForNextRoundRequest{
@@ -337,24 +336,26 @@ func (a *restClient) Ping(
} }
func (a *restClient) CreatePayment( func (a *restClient) CreatePayment(
ctx context.Context, inputs []client.Input, outputs []client.Output, ctx context.Context, inputs []client.AsyncPaymentInput, outputs []client.Output,
) (string, error) { ) (string, error) {
ins := make([]*models.V1Input, 0, len(inputs)) ins := make([]*models.V1AsyncPaymentInput, 0, len(inputs))
for _, i := range inputs { for _, i := range inputs {
ins = append(ins, &models.V1Input{ ins = append(ins, &models.V1AsyncPaymentInput{
Outpoint: &models.V1Outpoint{ Input: &models.V1Input{
Txid: i.Txid, Outpoint: &models.V1Outpoint{
Vout: int64(i.VOut), Txid: i.Input.Txid,
Vout: int64(i.VOut),
},
Descriptor: i.Input.Descriptor,
}, },
Descriptor: i.Descriptor, ForfeitLeafHash: i.ForfeitLeafHash.String(),
}) })
} }
outs := make([]*models.V1Output, 0, len(outputs)) outs := make([]*models.V1Output, 0, len(outputs))
for _, o := range outputs { for _, o := range outputs {
outs = append(outs, &models.V1Output{ outs = append(outs, &models.V1Output{
Address: o.Address, Address: o.Address,
Amount: strconv.Itoa(int(o.Amount)), Amount: strconv.Itoa(int(o.Amount)),
Descriptor: o.Descriptor,
}) })
} }
body := models.V1CreatePaymentRequest{ body := models.V1CreatePaymentRequest{
@@ -495,13 +496,13 @@ func (a *restClient) ListVtxos(
Txid: v.Outpoint.Txid, Txid: v.Outpoint.Txid,
VOut: uint32(v.Outpoint.Vout), VOut: uint32(v.Outpoint.Vout),
}, },
Amount: uint64(amount), Amount: uint64(amount),
RoundTxid: v.RoundTxid, RoundTxid: v.RoundTxid,
ExpiresAt: expiresAt, ExpiresAt: expiresAt,
Pending: v.Pending, Pending: v.Pending,
RedeemTx: v.RedeemTx, RedeemTx: v.RedeemTx,
SpentBy: v.SpentBy, SpentBy: v.SpentBy,
Descriptor: v.Descriptor, Pubkey: v.Pubkey,
}) })
} }
@@ -527,11 +528,11 @@ func (a *restClient) ListVtxos(
Txid: v.Outpoint.Txid, Txid: v.Outpoint.Txid,
VOut: uint32(v.Outpoint.Vout), VOut: uint32(v.Outpoint.Vout),
}, },
Amount: uint64(amount), Amount: uint64(amount),
RoundTxid: v.RoundTxid, RoundTxid: v.RoundTxid,
ExpiresAt: expiresAt, ExpiresAt: expiresAt,
SpentBy: v.SpentBy, SpentBy: v.SpentBy,
Descriptor: v.Descriptor, Pubkey: v.Pubkey,
}) })
} }
@@ -695,13 +696,13 @@ func vtxosFromRest(restVtxos []*models.V1Vtxo) []client.Vtxo {
Txid: v.Outpoint.Txid, Txid: v.Outpoint.Txid,
VOut: uint32(v.Outpoint.Vout), VOut: uint32(v.Outpoint.Vout),
}, },
Descriptor: v.Descriptor, Pubkey: v.Pubkey,
Amount: uint64(amount), Amount: uint64(amount),
RoundTxid: v.RoundTxid, RoundTxid: v.RoundTxid,
ExpiresAt: expiresAt, ExpiresAt: expiresAt,
RedeemTx: v.RedeemTx, RedeemTx: v.RedeemTx,
Pending: v.Pending, Pending: v.Pending,
SpentBy: v.SpentBy, SpentBy: v.SpentBy,
} }
} }
return vtxos return vtxos

View File

@@ -0,0 +1,112 @@
// Code generated by go-swagger; DO NOT EDIT.
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// V1AsyncPaymentInput v1 async payment input
//
// swagger:model v1AsyncPaymentInput
type V1AsyncPaymentInput struct {
// forfeit leaf hash
ForfeitLeafHash string `json:"forfeitLeafHash,omitempty"`
// input
Input *V1Input `json:"input,omitempty"`
}
// Validate validates this v1 async payment input
func (m *V1AsyncPaymentInput) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateInput(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *V1AsyncPaymentInput) validateInput(formats strfmt.Registry) error {
if swag.IsZero(m.Input) { // not required
return nil
}
if m.Input != nil {
if err := m.Input.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("input")
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("input")
}
return err
}
}
return nil
}
// ContextValidate validate this v1 async payment input based on the context it is used
func (m *V1AsyncPaymentInput) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
var res []error
if err := m.contextValidateInput(ctx, formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *V1AsyncPaymentInput) contextValidateInput(ctx context.Context, formats strfmt.Registry) error {
if m.Input != nil {
if swag.IsZero(m.Input) { // not required
return nil
}
if err := m.Input.ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("input")
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("input")
}
return err
}
}
return nil
}
// MarshalBinary interface implementation
func (m *V1AsyncPaymentInput) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *V1AsyncPaymentInput) UnmarshalBinary(b []byte) error {
var res V1AsyncPaymentInput
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -20,7 +20,7 @@ import (
type V1CreatePaymentRequest struct { type V1CreatePaymentRequest struct {
// inputs // inputs
Inputs []*V1Input `json:"inputs"` Inputs []*V1AsyncPaymentInput `json:"inputs"`
// outputs // outputs
Outputs []*V1Output `json:"outputs"` Outputs []*V1Output `json:"outputs"`

View File

@@ -22,9 +22,6 @@ type V1Output struct {
// Amount to send in satoshis. // Amount to send in satoshis.
Amount string `json:"amount,omitempty"` Amount string `json:"amount,omitempty"`
// descriptor
Descriptor string `json:"descriptor,omitempty"`
} }
// Validate validates this v1 output // Validate validates this v1 output

View File

@@ -21,9 +21,6 @@ type V1Vtxo struct {
// amount // amount
Amount string `json:"amount,omitempty"` Amount string `json:"amount,omitempty"`
// descriptor
Descriptor string `json:"descriptor,omitempty"`
// expire at // expire at
ExpireAt string `json:"expireAt,omitempty"` ExpireAt string `json:"expireAt,omitempty"`
@@ -33,6 +30,9 @@ type V1Vtxo struct {
// pending // pending
Pending bool `json:"pending,omitempty"` Pending bool `json:"pending,omitempty"`
// pubkey
Pubkey string `json:"pubkey,omitempty"`
// redeem tx // redeem tx
RedeemTx string `json:"redeemTx,omitempty"` RedeemTx string `json:"redeemTx,omitempty"`

View File

@@ -260,7 +260,7 @@ func (a *covenantArkClient) ListVtxos(
} }
for _, addr := range offchainAddrs { for _, addr := range offchainAddrs {
spendable, spent, err := a.client.ListVtxos(ctx, addr) spendable, spent, err := a.client.ListVtxos(ctx, addr.Address)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -303,7 +303,7 @@ func (a *covenantArkClient) Balance(
offchainBalance: balance, offchainBalance: balance,
offchainBalanceByExpiration: amountByExpiration, offchainBalanceByExpiration: amountByExpiration,
} }
}(offchainAddr) }(offchainAddr.Address)
getDelayedBalance := func(addr string) { getDelayedBalance := func(addr string) {
defer wg.Done() defer wg.Done()
@@ -323,8 +323,8 @@ func (a *covenantArkClient) Balance(
} }
} }
go getDelayedBalance(boardingAddr) go getDelayedBalance(boardingAddr.Address)
go getDelayedBalance(redeemAddr) go getDelayedBalance(redeemAddr.Address)
} }
wg.Wait() wg.Wait()
@@ -455,7 +455,7 @@ func (a *covenantArkClient) UnilateralRedeem(ctx context.Context) error {
vtxos := make([]client.Vtxo, 0) vtxos := make([]client.Vtxo, 0)
for _, offchainAddr := range offchainAddrs { for _, offchainAddr := range offchainAddrs {
fetchedVtxos, _, err := a.client.ListVtxos(ctx, offchainAddr) fetchedVtxos, _, err := a.client.ListVtxos(ctx, offchainAddr.Address)
if err != nil { if err != nil {
return err return err
} }
@@ -549,13 +549,19 @@ func (a *covenantArkClient) CollaborativeRedeem(
}, },
} }
vtxos := make([]client.Vtxo, 0) vtxos := make([]client.DescriptorVtxo, 0)
for _, offchainAddr := range offchainAddrs { for _, offchainAddr := range offchainAddrs {
spendableVtxos, err := a.getVtxos(ctx, offchainAddr, withExpiryCoinselect) spendableVtxos, err := a.getVtxos(ctx, offchainAddr.Address, withExpiryCoinselect)
if err != nil { if err != nil {
return "", err return "", err
} }
vtxos = append(vtxos, spendableVtxos...)
for _, vtxo := range spendableVtxos {
vtxos = append(vtxos, client.DescriptorVtxo{
Vtxo: vtxo,
Descriptor: offchainAddr.Descriptor,
})
}
} }
selectedCoins, changeAmount, err := utils.CoinSelect( selectedCoins, changeAmount, err := utils.CoinSelect(
@@ -571,14 +577,9 @@ func (a *covenantArkClient) CollaborativeRedeem(
return "", err return "", err
} }
desc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddr)
if err != nil {
return "", err
}
receivers = append(receivers, client.Output{ receivers = append(receivers, client.Output{
Descriptor: desc, Address: offchainAddr.Address,
Amount: changeAmount, Amount: changeAmount,
}) })
} }
@@ -621,12 +622,7 @@ func (a *covenantArkClient) SendAsync(
} }
func (a *covenantArkClient) Claim(ctx context.Context) (string, error) { func (a *covenantArkClient) Claim(ctx context.Context) (string, error) {
myselfOffchain, _, err := a.wallet.NewAddress(ctx, false) myselfOffchain, boardingAddr, err := a.wallet.NewAddress(ctx, false)
if err != nil {
return "", err
}
_, mypubkey, _, err := common.DecodeAddress(myselfOffchain)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -644,21 +640,16 @@ func (a *covenantArkClient) Claim(ctx context.Context) (string, error) {
return "", fmt.Errorf("no funds to claim") return "", fmt.Errorf("no funds to claim")
} }
desc, err := a.offchainAddressToDefaultVtxoDescriptor(myselfOffchain)
if err != nil {
return "", err
}
receiver := client.Output{ receiver := client.Output{
Descriptor: desc, Address: myselfOffchain.Address,
Amount: pendingBalance, Amount: pendingBalance,
} }
return a.selfTransferAllPendingPayments( return a.selfTransferAllPendingPayments(
ctx, ctx,
boardingUtxos, boardingUtxos,
receiver, receiver,
hex.EncodeToString(mypubkey.SerializeCompressed()), boardingAddr.Descriptor,
) )
} }
@@ -692,13 +683,13 @@ func (a *covenantArkClient) getAllBoardingUtxos(ctx context.Context) ([]explorer
utxos := []explorer.Utxo{} utxos := []explorer.Utxo{}
for _, addr := range boardingAddrs { for _, addr := range boardingAddrs {
txs, err := a.explorer.GetTxs(addr) txs, err := a.explorer.GetTxs(addr.Address)
if err != nil { if err != nil {
continue continue
} }
for _, tx := range txs { for _, tx := range txs {
for i, vout := range tx.Vout { for i, vout := range tx.Vout {
if vout.Address == addr { if vout.Address == addr.Address {
createdAt := time.Time{} createdAt := time.Time{}
if tx.Status.Confirmed { if tx.Status.Confirmed {
createdAt = time.Unix(tx.Status.Blocktime, 0) createdAt = time.Unix(tx.Status.Blocktime, 0)
@@ -718,43 +709,34 @@ func (a *covenantArkClient) getAllBoardingUtxos(ctx context.Context) ([]explorer
} }
func (a *covenantArkClient) getClaimableBoardingUtxos(ctx context.Context) ([]explorer.Utxo, error) { func (a *covenantArkClient) getClaimableBoardingUtxos(ctx context.Context) ([]explorer.Utxo, error) {
offchainAddrs, boardingAddrs, _, err := a.wallet.GetAddresses(ctx) _, boardingAddrs, _, err := a.wallet.GetAddresses(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
claimable := make([]explorer.Utxo, 0) claimable := make([]explorer.Utxo, 0)
now := time.Now()
_, myPubkey, _, err := common.DecodeAddress(offchainAddrs[0])
if err != nil {
return nil, err
}
myPubkeyStr := hex.EncodeToString(schnorr.SerializePubKey(myPubkey))
descriptorStr := strings.ReplaceAll(
a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
)
boardingScript, err := tree.ParseVtxoScript(descriptorStr)
if err != nil {
return nil, err
}
var boardingTimeout uint
if defaultVtxo, ok := boardingScript.(*tree.DefaultVtxoScript); ok {
boardingTimeout = defaultVtxo.ExitDelay
} else {
return nil, fmt.Errorf("unsupported boarding descriptor: %s", descriptorStr)
}
for _, addr := range boardingAddrs { for _, addr := range boardingAddrs {
boardingUtxos, err := a.explorer.GetUtxos(addr) boardingScript, err := tree.ParseVtxoScript(addr.Descriptor)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var boardingTimeout uint
if defaultVtxo, ok := boardingScript.(*tree.DefaultVtxoScript); ok {
boardingTimeout = defaultVtxo.ExitDelay
} else {
return nil, fmt.Errorf("unsupported boarding descriptor: %s", addr.Descriptor)
}
boardingUtxos, err := a.explorer.GetUtxos(addr.Address)
if err != nil {
return nil, err
}
now := time.Now()
for _, utxo := range boardingUtxos { for _, utxo := range boardingUtxos {
u := utxo.ToUtxo(boardingTimeout) u := utxo.ToUtxo(boardingTimeout)
@@ -826,7 +808,7 @@ func (a *covenantArkClient) sendOnchain(
return "", err return "", err
} }
changeScript, err := address.ToOutputScript(changeAddr) changeScript, err := address.ToOutputScript(changeAddr.Address)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -876,7 +858,7 @@ func (a *covenantArkClient) sendOnchain(
return "", err return "", err
} }
changeScript, err := address.ToOutputScript(changeAddr) changeScript, err := address.ToOutputScript(changeAddr.Address)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -938,49 +920,47 @@ func (a *covenantArkClient) sendOffchain(
return "", fmt.Errorf("no funds detected") return "", fmt.Errorf("no funds detected")
} }
_, _, aspPubKey, err := common.DecodeAddress(offchainAddrs[0]) expectedAspPubkey := schnorr.SerializePubKey(a.AspPubkey)
if err != nil {
return "", err
}
receiversOutput := make([]client.Output, 0) receiversOutput := make([]client.Output, 0)
sumOfReceivers := uint64(0) sumOfReceivers := uint64(0)
for _, receiver := range receivers { for _, receiver := range receivers {
_, _, aspKey, err := common.DecodeAddress(receiver.To()) rcvAddr, err := common.DecodeAddress(receiver.To())
if err != nil { if err != nil {
return "", fmt.Errorf("invalid receiver address: %s", err) return "", fmt.Errorf("invalid receiver address: %s", err)
} }
if !bytes.Equal( rcvAspPubkey := schnorr.SerializePubKey(rcvAddr.Asp)
aspPubKey.SerializeCompressed(), aspKey.SerializeCompressed(),
) { if !bytes.Equal(rcvAspPubkey, expectedAspPubkey) {
return "", fmt.Errorf("invalid receiver address '%s': must be associated with the connected service provider", receiver.To()) return "", fmt.Errorf("invalid receiver address '%s': expected ASP %s, got %s", receiver.To(), hex.EncodeToString(expectedAspPubkey), hex.EncodeToString(rcvAspPubkey))
} }
if receiver.Amount() < a.Dust { if receiver.Amount() < a.Dust {
return "", fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount(), a.Dust) return "", fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount(), a.Dust)
} }
desc, err := a.offchainAddressToDefaultVtxoDescriptor(receiver.To())
if err != nil {
return "", err
}
receiversOutput = append(receiversOutput, client.Output{ receiversOutput = append(receiversOutput, client.Output{
Descriptor: desc, Address: receiver.To(),
Amount: receiver.Amount(), Amount: receiver.Amount(),
}) })
sumOfReceivers += receiver.Amount() sumOfReceivers += receiver.Amount()
} }
vtxos := make([]client.Vtxo, 0) vtxos := make([]client.DescriptorVtxo, 0)
for _, offchainAddr := range offchainAddrs { for _, offchainAddr := range offchainAddrs {
spendableVtxos, err := a.getVtxos(ctx, offchainAddr, withExpiryCoinselect) spendableVtxos, err := a.getVtxos(ctx, offchainAddr.Address, withExpiryCoinselect)
if err != nil { if err != nil {
return "", err return "", err
} }
vtxos = append(vtxos, spendableVtxos...)
for _, vtxo := range spendableVtxos {
vtxos = append(vtxos, client.DescriptorVtxo{
Vtxo: vtxo,
Descriptor: offchainAddr.Descriptor,
})
}
} }
selectedCoins, changeAmount, err := utils.CoinSelect( selectedCoins, changeAmount, err := utils.CoinSelect(
@@ -996,14 +976,9 @@ func (a *covenantArkClient) sendOffchain(
return "", err return "", err
} }
desc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddr)
if err != nil {
return "", err
}
changeReceiver := client.Output{ changeReceiver := client.Output{
Descriptor: desc, Address: offchainAddr.Address,
Amount: changeAmount, Amount: changeAmount,
} }
receiversOutput = append(receiversOutput, changeReceiver) receiversOutput = append(receiversOutput, changeReceiver)
} }
@@ -1044,6 +1019,7 @@ func (a *covenantArkClient) sendOffchain(
return poolTxID, nil return poolTxID, nil
} }
// addInputs adds the inputs to the pset for send onchain
func (a *covenantArkClient) addInputs( func (a *covenantArkClient) addInputs(
ctx context.Context, ctx context.Context,
updater *psetv2.Updater, updater *psetv2.Updater,
@@ -1055,11 +1031,21 @@ func (a *covenantArkClient) addInputs(
return err return err
} }
_, userPubkey, aspPubkey, err := common.DecodeAddress(offchain) vtxoScript, err := tree.ParseVtxoScript(offchain.Descriptor)
if err != nil { if err != nil {
return err return err
} }
var userPubkey, aspPubkey *secp256k1.PublicKey
switch s := vtxoScript.(type) {
case *tree.DefaultVtxoScript:
userPubkey = s.Owner
aspPubkey = s.Asp
default:
return fmt.Errorf("unsupported vtxo script: %T", s)
}
for _, utxo := range utxos { for _, utxo := range utxos {
sequence, err := utxo.Sequence() sequence, err := utxo.Sequence()
if err != nil { if err != nil {
@@ -1126,7 +1112,7 @@ func (a *covenantArkClient) addInputs(
func (a *covenantArkClient) handleRoundStream( func (a *covenantArkClient) handleRoundStream(
ctx context.Context, ctx context.Context,
paymentID string, paymentID string,
vtxosToSign []client.Vtxo, vtxosToSign []client.DescriptorVtxo,
boardingUtxos []explorer.Utxo, boardingUtxos []explorer.Utxo,
boardingDescriptor string, boardingDescriptor string,
receivers []client.Output, receivers []client.Output,
@@ -1191,7 +1177,7 @@ func (a *covenantArkClient) handleRoundStream(
func (a *covenantArkClient) handleRoundFinalization( func (a *covenantArkClient) handleRoundFinalization(
ctx context.Context, ctx context.Context,
event client.RoundFinalizationEvent, event client.RoundFinalizationEvent,
vtxos []client.Vtxo, vtxos []client.DescriptorVtxo,
boardingUtxos []explorer.Utxo, boardingUtxos []explorer.Utxo,
boardingDescriptor string, boardingDescriptor string,
receivers []client.Output, receivers []client.Output,
@@ -1200,18 +1186,8 @@ func (a *covenantArkClient) handleRoundFinalization(
return return
} }
offchainAddr, _, err := a.wallet.NewAddress(ctx, false)
if err != nil {
return
}
_, myPubkey, _, err := common.DecodeAddress(offchainAddr)
if err != nil {
return
}
if len(vtxos) > 0 { if len(vtxos) > 0 {
signedForfeits, err = a.createAndSignForfeits(ctx, vtxos, event.Connectors, event.MinRelayFeeRate, myPubkey) signedForfeits, err = a.createAndSignForfeits(ctx, vtxos, event.Connectors, event.MinRelayFeeRate)
if err != nil { if err != nil {
return return
} }
@@ -1228,10 +1204,16 @@ func (a *covenantArkClient) handleRoundFinalization(
return nil, "", err return nil, "", err
} }
// add tapscript leaf var forfeitClosure tree.Closure
forfeitClosure := &tree.MultisigClosure{
Pubkey: myPubkey, switch s := boardingVtxoScript.(type) {
AspPubkey: a.AspPubkey, case *tree.DefaultVtxoScript:
forfeitClosure = &tree.MultisigClosure{
Pubkey: s.Owner,
AspPubkey: a.AspPubkey,
}
default:
return nil, "", fmt.Errorf("unsupported boarding descriptor: %s", boardingDescriptor)
} }
forfeitLeaf, err := forfeitClosure.Leaf() forfeitLeaf, err := forfeitClosure.Leaf()
@@ -1381,15 +1363,12 @@ func (a *covenantArkClient) validateOffChainReceiver(
) error { ) error {
found := false found := false
receiverVtxoScript, err := tree.ParseVtxoScript(receiver.Descriptor) addr, err := common.DecodeAddress(receiver.Address)
if err != nil { if err != nil {
return err return err
} }
outputTapKey, _, err := receiverVtxoScript.TapTree() vtxoTapKey := schnorr.SerializePubKey(addr.VtxoTapKey)
if err != nil {
return err
}
leaves := congestionTree.Leaves() leaves := congestionTree.Leaves()
for _, leaf := range leaves { for _, leaf := range leaves {
@@ -1402,7 +1381,7 @@ func (a *covenantArkClient) validateOffChainReceiver(
if len(output.Script) == 0 { if len(output.Script) == 0 {
continue continue
} }
if bytes.Equal(output.Script[2:], schnorr.SerializePubKey(outputTapKey)) { if bytes.Equal(output.Script[2:], vtxoTapKey) {
if output.Value == receiver.Amount { if output.Value == receiver.Amount {
found = true found = true
break break
@@ -1423,10 +1402,9 @@ func (a *covenantArkClient) validateOffChainReceiver(
func (a *covenantArkClient) createAndSignForfeits( func (a *covenantArkClient) createAndSignForfeits(
ctx context.Context, ctx context.Context,
vtxosToSign []client.Vtxo, vtxosToSign []client.DescriptorVtxo,
connectors []string, connectors []string,
feeRate chainfee.SatPerKVByte, feeRate chainfee.SatPerKVByte,
myPubKey *secp256k1.PublicKey,
) ([]string, error) { ) ([]string, error) {
signedForfeits := make([]string, 0) signedForfeits := make([]string, 0)
connectorsPsets := make([]*psetv2.Pset, 0, len(connectors)) connectorsPsets := make([]*psetv2.Pset, 0, len(connectors))
@@ -1471,9 +1449,16 @@ func (a *covenantArkClient) createAndSignForfeits(
TxIndex: vtxo.VOut, TxIndex: vtxo.VOut,
} }
forfeitClosure := &tree.MultisigClosure{ var forfeitClosure tree.Closure
Pubkey: myPubKey,
AspPubkey: a.AspPubkey, switch s := vtxoScript.(type) {
case *tree.DefaultVtxoScript:
forfeitClosure = &tree.MultisigClosure{
Pubkey: s.Owner,
AspPubkey: a.AspPubkey,
}
default:
return nil, fmt.Errorf("unsupported vtxo script: %T", s)
} }
forfeitLeaf, err := forfeitClosure.Leaf() forfeitLeaf, err := forfeitClosure.Leaf()
@@ -1535,38 +1520,31 @@ func (a *covenantArkClient) createAndSignForfeits(
func (a *covenantArkClient) coinSelectOnchain( func (a *covenantArkClient) coinSelectOnchain(
ctx context.Context, targetAmount uint64, exclude []explorer.Utxo, ctx context.Context, targetAmount uint64, exclude []explorer.Utxo,
) ([]explorer.Utxo, uint64, error) { ) ([]explorer.Utxo, uint64, error) {
offchainAddrs, boardingAddrs, redemptionAddrs, err := a.wallet.GetAddresses(ctx) _, boardingAddrs, redemptionAddrs, err := a.wallet.GetAddresses(ctx)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
_, myPubkey, _, err := common.DecodeAddress(offchainAddrs[0])
if err != nil {
return nil, 0, err
}
myPubkeyStr := hex.EncodeToString(schnorr.SerializePubKey(myPubkey))
descriptorStr := strings.ReplaceAll(
a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
)
boardingScript, err := tree.ParseVtxoScript(descriptorStr)
if err != nil {
return nil, 0, err
}
var boardingTimeout uint
if defaultVtxo, ok := boardingScript.(*tree.DefaultVtxoScript); ok {
boardingTimeout = defaultVtxo.ExitDelay
} else {
return nil, 0, fmt.Errorf("unsupported boarding descriptor: %s", descriptorStr)
}
now := time.Now() now := time.Now()
fetchedUtxos := make([]explorer.Utxo, 0) fetchedUtxos := make([]explorer.Utxo, 0)
for _, addr := range boardingAddrs { for _, addr := range boardingAddrs {
utxos, err := a.explorer.GetUtxos(addr) boardingDescriptor := addr.Descriptor
boardingScript, err := tree.ParseVtxoScript(boardingDescriptor)
if err != nil {
return nil, 0, err
}
var boardingTimeout uint
if defaultVtxo, ok := boardingScript.(*tree.DefaultVtxoScript); ok {
boardingTimeout = defaultVtxo.ExitDelay
} else {
return nil, 0, fmt.Errorf("unsupported boarding descriptor: %s", boardingDescriptor)
}
utxos, err := a.explorer.GetUtxos(addr.Address)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
@@ -1602,7 +1580,7 @@ func (a *covenantArkClient) coinSelectOnchain(
fetchedUtxos = make([]explorer.Utxo, 0) fetchedUtxos = make([]explorer.Utxo, 0)
for _, addr := range redemptionAddrs { for _, addr := range redemptionAddrs {
utxos, err := a.explorer.GetUtxos(addr) utxos, err := a.explorer.GetUtxos(addr.Address)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
@@ -1731,14 +1709,10 @@ func (a *covenantArkClient) getVtxos(
} }
func (a *covenantArkClient) selfTransferAllPendingPayments( func (a *covenantArkClient) selfTransferAllPendingPayments(
ctx context.Context, boardingUtxos []explorer.Utxo, myself client.Output, mypubkey string, ctx context.Context, boardingUtxos []explorer.Utxo, myself client.Output, boardingDescriptor string,
) (string, error) { ) (string, error) {
inputs := make([]client.Input, 0, len(boardingUtxos)) inputs := make([]client.Input, 0, len(boardingUtxos))
boardingDescriptor := strings.ReplaceAll(
a.BoardingDescriptorTemplate, "USER", mypubkey[2:],
)
for _, utxo := range boardingUtxos { for _, utxo := range boardingUtxos {
inputs = append(inputs, client.Input{ inputs = append(inputs, client.Input{
Outpoint: client.Outpoint{ Outpoint: client.Outpoint{
@@ -1761,7 +1735,7 @@ func (a *covenantArkClient) selfTransferAllPendingPayments(
} }
roundTxid, err := a.handleRoundStream( roundTxid, err := a.handleRoundStream(
ctx, paymentID, make([]client.Vtxo, 0), boardingUtxos, boardingDescriptor, outputs, ctx, paymentID, make([]client.DescriptorVtxo, 0), boardingUtxos, boardingDescriptor, outputs,
) )
if err != nil { if err != nil {
return "", err return "", err
@@ -1770,24 +1744,7 @@ func (a *covenantArkClient) selfTransferAllPendingPayments(
return roundTxid, nil return roundTxid, nil
} }
func (a *covenantArkClient) offchainAddressToDefaultVtxoDescriptor(addr string) (string, error) { func (a *covenantArkClient) getBoardingTxs(ctx context.Context) (transactions []types.Transaction) {
_, userPubKey, aspPubkey, err := common.DecodeAddress(addr)
if err != nil {
return "", err
}
vtxoScript := tree.DefaultVtxoScript{
Owner: userPubKey,
Asp: aspPubkey,
ExitDelay: uint(a.UnilateralExitDelay),
}
return vtxoScript.ToDescriptor(), nil
}
func (a *covenantArkClient) getBoardingTxs(
ctx context.Context,
) (transactions []types.Transaction) {
utxos, err := a.getClaimableBoardingUtxos(ctx) utxos, err := a.getClaimableBoardingUtxos(ctx)
if err != nil { if err != nil {
return nil return nil

View File

@@ -223,12 +223,14 @@ func (a *covenantlessArkClient) listenForTransactions(ctx context.Context) {
return return
} }
desc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddr) addr, err := common.DecodeAddress(offchainAddr.Address)
if err != nil { if err != nil {
log.WithError(err).Error("Failed to get descriptor for new address") log.WithError(err).Error("Failed to decode address")
return return
} }
addrPubkey := hex.EncodeToString(schnorr.SerializePubKey(addr.VtxoTapKey))
for { for {
select { select {
case event, ok := <-eventChan: case event, ok := <-eventChan:
@@ -253,7 +255,7 @@ func (a *covenantlessArkClient) listenForTransactions(ctx context.Context) {
continue continue
} }
a.processTransactionEvent(desc, event, pendingBoardingTxsMap) a.processTransactionEvent(addrPubkey, event, pendingBoardingTxsMap)
case <-ctx.Done(): case <-ctx.Done():
return return
} }
@@ -319,7 +321,7 @@ func (a *covenantlessArkClient) getBoardingPendingTransactions(
} }
func (a *covenantlessArkClient) processTransactionEvent( func (a *covenantlessArkClient) processTransactionEvent(
descriptor string, pubkey string,
event client.TransactionEvent, event client.TransactionEvent,
pendingBoardingTxsMap map[string]types.Transaction, pendingBoardingTxsMap map[string]types.Transaction,
) { ) {
@@ -372,7 +374,7 @@ func (a *covenantlessArkClient) processTransactionEvent(
vtxosToInsert := make([]types.Vtxo, 0) vtxosToInsert := make([]types.Vtxo, 0)
txsToInsert := make([]types.Transaction, 0) txsToInsert := make([]types.Transaction, 0)
for _, v := range event.Round.SpendableVtxos { for _, v := range event.Round.SpendableVtxos {
if v.Descriptor == descriptor { if v.Pubkey == pubkey {
vtxosToInsert = append(vtxosToInsert, types.Vtxo{ vtxosToInsert = append(vtxosToInsert, types.Vtxo{
VtxoKey: types.VtxoKey{ VtxoKey: types.VtxoKey{
Txid: v.Txid, Txid: v.Txid,
@@ -449,7 +451,7 @@ func (a *covenantlessArkClient) processTransactionEvent(
outputAmount := uint64(0) outputAmount := uint64(0)
for _, v := range event.Redeem.SpendableVtxos { for _, v := range event.Redeem.SpendableVtxos {
if v.Descriptor == descriptor { if v.Pubkey == pubkey {
vtxosToInsert = append(vtxosToInsert, types.Vtxo{ vtxosToInsert = append(vtxosToInsert, types.Vtxo{
VtxoKey: types.VtxoKey{ VtxoKey: types.VtxoKey{
Txid: v.Txid, Txid: v.Txid,
@@ -482,7 +484,7 @@ func (a *covenantlessArkClient) processTransactionEvent(
} }
} else { } else {
for _, v := range event.Redeem.SpendableVtxos { for _, v := range event.Redeem.SpendableVtxos {
if v.Descriptor == descriptor { if v.Pubkey == pubkey {
vtxosToInsert = append(vtxosToInsert, types.Vtxo{ vtxosToInsert = append(vtxosToInsert, types.Vtxo{
VtxoKey: types.VtxoKey{ VtxoKey: types.VtxoKey{
Txid: v.Txid, Txid: v.Txid,
@@ -524,17 +526,25 @@ func (a *covenantlessArkClient) processTransactionEvent(
func (a *covenantlessArkClient) ListVtxos( func (a *covenantlessArkClient) ListVtxos(
ctx context.Context, ctx context.Context,
) (spendableVtxos, spentVtxos []client.Vtxo, err error) { ) (spendableVtxos, spentVtxos []client.Vtxo, err error) {
offchainAddrs, _, _, err := a.wallet.GetAddresses(ctx) offchainAddrs, boardingAddrs, _, err := a.wallet.GetAddresses(ctx)
if err != nil { if err != nil {
return return
} }
_, pubkey, _, err := common.DecodeAddress(offchainAddrs[0]) boardingAddrScript, err := bitcointree.ParseVtxoScript(boardingAddrs[0].Descriptor)
if err != nil { if err != nil {
return return
} }
myPubkey := schnorr.SerializePubKey(pubkey) var myPubkey []byte
if boardingScript, ok := boardingAddrScript.(*bitcointree.DefaultVtxoScript); ok {
myPubkey = schnorr.SerializePubKey(boardingScript.Owner)
}
if myPubkey == nil {
return nil, nil, fmt.Errorf("invalid boarding address descriptor")
}
// The ASP returns the vtxos sent to others via async payments as spendable // The ASP returns the vtxos sent to others via async payments as spendable
// because they are actually revertable. Since we do not provide any revert // because they are actually revertable. Since we do not provide any revert
@@ -544,22 +554,25 @@ func (a *covenantlessArkClient) ListVtxos(
// The auxiliary variables below are used to make these checks in an // The auxiliary variables below are used to make these checks in an
// efficient way. // efficient way.
for _, addr := range offchainAddrs { for _, addr := range offchainAddrs {
spendable, spent, err := a.client.ListVtxos(ctx, addr) spendable, spent, err := a.client.ListVtxos(ctx, addr.Address)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
script, err := bitcointree.ParseVtxoScript(addr.Descriptor)
if err != nil {
return nil, nil, err
}
reversibleVtxo, isReversible := script.(*bitcointree.ReversibleVtxoScript)
for _, v := range spendable { for _, v := range spendable {
if !v.Pending { if !v.Pending {
spendableVtxos = append(spendableVtxos, v) spendableVtxos = append(spendableVtxos, v)
continue continue
} }
script, err := bitcointree.ParseVtxoScript(v.Descriptor)
if err != nil {
return nil, nil, err
}
reversibleVtxo, ok := script.(*bitcointree.ReversibleVtxoScript) if !isReversible {
if !ok {
spendableVtxos = append(spendableVtxos, v) spendableVtxos = append(spendableVtxos, v)
continue continue
} }
@@ -568,17 +581,13 @@ func (a *covenantlessArkClient) ListVtxos(
spendableVtxos = append(spendableVtxos, v) spendableVtxos = append(spendableVtxos, v)
} }
} }
for _, v := range spent {
script, err := bitcointree.ParseVtxoScript(v.Descriptor)
if err != nil {
return nil, nil, err
}
reversibleVtxo, ok := script.(*bitcointree.ReversibleVtxoScript) for _, v := range spent {
if !ok { if !isReversible {
spentVtxos = append(spentVtxos, v) spentVtxos = append(spentVtxos, v)
continue continue
} }
if !bytes.Equal(schnorr.SerializePubKey(reversibleVtxo.Sender), myPubkey) { if !bytes.Equal(schnorr.SerializePubKey(reversibleVtxo.Sender), myPubkey) {
spentVtxos = append(spentVtxos, v) spentVtxos = append(spentVtxos, v)
} }
@@ -602,14 +611,13 @@ func (a *covenantlessArkClient) Balance(
chRes := make(chan balanceRes, nbWorkers*len(offchainAddrs)) chRes := make(chan balanceRes, nbWorkers*len(offchainAddrs))
for i := range offchainAddrs { for i := range offchainAddrs {
offchainAddr := offchainAddrs[i]
boardingAddr := boardingAddrs[i] boardingAddr := boardingAddrs[i]
redeemAddr := redeemAddrs[i] redeemAddr := redeemAddrs[i]
go func(addr string) { go func() {
defer wg.Done() defer wg.Done()
balance, amountByExpiration, err := a.getOffchainBalance( balance, amountByExpiration, err := a.getOffchainBalance(
ctx, addr, computeVtxoExpiration, ctx, computeVtxoExpiration,
) )
if err != nil { if err != nil {
chRes <- balanceRes{err: err} chRes <- balanceRes{err: err}
@@ -620,7 +628,7 @@ func (a *covenantlessArkClient) Balance(
offchainBalance: balance, offchainBalance: balance,
offchainBalanceByExpiration: amountByExpiration, offchainBalanceByExpiration: amountByExpiration,
} }
}(offchainAddr) }()
getDelayedBalance := func(addr string) { getDelayedBalance := func(addr string) {
defer wg.Done() defer wg.Done()
@@ -640,8 +648,8 @@ func (a *covenantlessArkClient) Balance(
} }
} }
go getDelayedBalance(boardingAddr) go getDelayedBalance(boardingAddr.Address)
go getDelayedBalance(redeemAddr) go getDelayedBalance(redeemAddr.Address)
} }
wg.Wait() wg.Wait()
@@ -765,20 +773,11 @@ func (a *covenantlessArkClient) UnilateralRedeem(ctx context.Context) error {
return fmt.Errorf("wallet is locked") return fmt.Errorf("wallet is locked")
} }
offchainAddrs, _, _, err := a.wallet.GetAddresses(ctx) vtxos, _, err := a.getVtxos(ctx, false)
if err != nil { if err != nil {
return err return err
} }
vtxos := make([]client.Vtxo, 0)
for _, offchainAddr := range offchainAddrs {
spendableVtxos, _, err := a.getVtxos(ctx, offchainAddr, false)
if err != nil {
return err
}
vtxos = append(vtxos, spendableVtxos...)
}
totalVtxosAmount := uint64(0) totalVtxosAmount := uint64(0)
for _, vtxo := range vtxos { for _, vtxo := range vtxos {
totalVtxosAmount += vtxo.Amount totalVtxosAmount += vtxo.Amount
@@ -853,13 +852,26 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
}, },
} }
vtxos := make([]client.Vtxo, 0) vtxos := make([]client.DescriptorVtxo, 0)
spendableVtxos, _, err := a.getVtxos(ctx, withExpiryCoinselect)
if err != nil {
return "", err
}
for _, offchainAddr := range offchainAddrs { for _, offchainAddr := range offchainAddrs {
spendableVtxos, _, err := a.getVtxos(ctx, offchainAddr, withExpiryCoinselect) for _, v := range spendableVtxos {
if err != nil { vtxoAddr, err := v.Address(a.AspPubkey, a.Network)
return "", err if err != nil {
return "", err
}
if vtxoAddr == offchainAddr.Address {
vtxos = append(vtxos, client.DescriptorVtxo{
Vtxo: v,
Descriptor: offchainAddr.Descriptor,
})
}
} }
vtxos = append(vtxos, spendableVtxos...)
} }
selectedCoins, changeAmount, err := utils.CoinSelect( selectedCoins, changeAmount, err := utils.CoinSelect(
@@ -875,14 +887,9 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
return "", err return "", err
} }
desc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddr)
if err != nil {
return "", err
}
receivers = append(receivers, client.Output{ receivers = append(receivers, client.Output{
Descriptor: desc, Address: offchainAddr.Address,
Amount: changeAmount, Amount: changeAmount,
}) })
} }
@@ -950,59 +957,57 @@ func (a *covenantlessArkClient) SendAsync(
return "", err return "", err
} }
_, _, aspPubKey, err := common.DecodeAddress(offchainAddrs[0]) expectedAspPubKey := schnorr.SerializePubKey(a.AspPubkey)
if err != nil {
return "", err
}
receiversOutput := make([]client.Output, 0) receiversOutput := make([]client.Output, 0)
sumOfReceivers := uint64(0) sumOfReceivers := uint64(0)
for _, receiver := range receivers { for _, receiver := range receivers {
_, _, aspKey, err := common.DecodeAddress(receiver.To()) rcvAddr, err := common.DecodeAddress(receiver.To())
if err != nil { if err != nil {
return "", fmt.Errorf("invalid receiver address: %s", err) return "", fmt.Errorf("invalid receiver address: %s", err)
} }
if !bytes.Equal( rcvAspPubKey := schnorr.SerializePubKey(rcvAddr.Asp)
aspPubKey.SerializeCompressed(), aspKey.SerializeCompressed(),
) { if !bytes.Equal(expectedAspPubKey, rcvAspPubKey) {
return "", fmt.Errorf("invalid receiver address '%s': must be associated with the connected service provider", receiver) return "", fmt.Errorf("invalid receiver address '%s': expected ASP %s, got %s", receiver.To(), hex.EncodeToString(expectedAspPubKey), hex.EncodeToString(rcvAspPubKey))
} }
if receiver.Amount() < a.Dust { if receiver.Amount() < a.Dust {
return "", fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount(), a.Dust) return "", fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount(), a.Dust)
} }
isSelfTransfer := offchainAddrs[0] == receiver.To()
var desc string
// reversible vtxo does not make sense for self transfer
// if the receiver is the same as the sender, handle the output like the change
if !isSelfTransfer {
desc, err = a.offchainAddressToReversibleVtxoDescriptor(offchainAddrs[0], receiver.To())
if err != nil {
return "", err
}
} else {
desc, err = a.offchainAddressToDefaultVtxoDescriptor(receiver.To())
if err != nil {
return "", err
}
}
receiversOutput = append(receiversOutput, client.Output{ receiversOutput = append(receiversOutput, client.Output{
Descriptor: desc, Address: receiver.To(),
Amount: receiver.Amount(), Amount: receiver.Amount(),
}) })
sumOfReceivers += receiver.Amount() sumOfReceivers += receiver.Amount()
} }
vtxos, _, err := a.getVtxos(ctx, offchainAddrs[0], withExpiryCoinselect) vtxos := make([]client.DescriptorVtxo, 0)
spendableVtxos, _, err := a.getVtxos(ctx, withExpiryCoinselect)
if err != nil { if err != nil {
return "", err return "", err
} }
for _, offchainAddr := range offchainAddrs {
for _, v := range spendableVtxos {
vtxoAddr, err := v.Address(a.AspPubkey, a.Network)
if err != nil {
return "", err
}
if vtxoAddr == offchainAddr.Address {
vtxos = append(vtxos, client.DescriptorVtxo{
Vtxo: v,
Descriptor: offchainAddr.Descriptor,
})
}
}
}
selectedCoins, changeAmount, err := utils.CoinSelect( selectedCoins, changeAmount, err := utils.CoinSelect(
vtxos, sumOfReceivers, a.Dust, withExpiryCoinselect, vtxos, sumOfReceivers, a.Dust, withExpiryCoinselect,
) )
@@ -1011,27 +1016,52 @@ func (a *covenantlessArkClient) SendAsync(
} }
if changeAmount > 0 { if changeAmount > 0 {
changeDesc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddrs[0])
if err != nil {
return "", err
}
changeReceiver := client.Output{ changeReceiver := client.Output{
Descriptor: changeDesc, Address: offchainAddrs[0].Address,
Amount: changeAmount, Amount: changeAmount,
} }
receiversOutput = append(receiversOutput, changeReceiver) receiversOutput = append(receiversOutput, changeReceiver)
} }
inputs := make([]client.Input, 0, len(selectedCoins)) inputs := make([]client.AsyncPaymentInput, 0, len(selectedCoins))
for _, coin := range selectedCoins { for _, coin := range selectedCoins {
inputs = append(inputs, client.Input{ vtxoScript, err := bitcointree.ParseVtxoScript(coin.Descriptor)
Outpoint: client.Outpoint{ if err != nil {
Txid: coin.Txid, return "", err
VOut: coin.VOut, }
var forfeitClosure bitcointree.Closure
switch s := vtxoScript.(type) {
case *bitcointree.DefaultVtxoScript:
forfeitClosure = &bitcointree.MultisigClosure{
Pubkey: s.Owner,
AspPubkey: s.Asp,
}
case *bitcointree.ReversibleVtxoScript:
forfeitClosure = &bitcointree.MultisigClosure{
Pubkey: s.Owner,
AspPubkey: s.Asp,
}
default:
return "", fmt.Errorf("unsupported vtxo script: %T", s)
}
forfeitLeaf, err := forfeitClosure.Leaf()
if err != nil {
return "", err
}
inputs = append(inputs, client.AsyncPaymentInput{
Input: client.Input{
Outpoint: client.Outpoint{
Txid: coin.Txid,
VOut: coin.VOut,
},
Descriptor: coin.Descriptor,
}, },
Descriptor: coin.Descriptor, ForfeitLeafHash: forfeitLeaf.TapHash(),
}) })
} }
@@ -1057,19 +1087,22 @@ func (a *covenantlessArkClient) SendAsync(
} }
func (a *covenantlessArkClient) Claim(ctx context.Context) (string, error) { func (a *covenantlessArkClient) Claim(ctx context.Context) (string, error) {
myselfOffchain, _, err := a.wallet.NewAddress(ctx, false) myselfOffchain, boardingAddr, err := a.wallet.NewAddress(ctx, false)
if err != nil { if err != nil {
return "", err return "", err
} }
_, pendingVtxos, err := a.getVtxos(ctx, myselfOffchain, false) _, pendingVtxos, err := a.getVtxos(ctx, false)
if err != nil { if err != nil {
return "", err return "", err
} }
_, mypubkey, _, err := common.DecodeAddress(myselfOffchain) pendingVtxosWithDescriptor := make([]client.DescriptorVtxo, 0)
if err != nil { for _, vtxo := range pendingVtxos {
return "", err pendingVtxosWithDescriptor = append(pendingVtxosWithDescriptor, client.DescriptorVtxo{
Vtxo: vtxo,
Descriptor: myselfOffchain.Descriptor,
})
} }
boardingUtxos, err := a.getClaimableBoardingUtxos(ctx) boardingUtxos, err := a.getClaimableBoardingUtxos(ctx)
@@ -1088,22 +1121,17 @@ func (a *covenantlessArkClient) Claim(ctx context.Context) (string, error) {
return "", nil return "", nil
} }
desc, err := a.offchainAddressToDefaultVtxoDescriptor(myselfOffchain)
if err != nil {
return "", err
}
receiver := client.Output{ receiver := client.Output{
Descriptor: desc, Address: myselfOffchain.Address,
Amount: pendingBalance, Amount: pendingBalance,
} }
return a.selfTransferAllPendingPayments( return a.selfTransferAllPendingPayments(
ctx, ctx,
pendingVtxos, pendingVtxosWithDescriptor,
boardingUtxos, boardingUtxos,
receiver, receiver,
hex.EncodeToString(mypubkey.SerializeCompressed()), boardingAddr.Descriptor,
) )
} }
@@ -1208,7 +1236,7 @@ func (a *covenantlessArkClient) sendOnchain(
if err != nil { if err != nil {
return "", err return "", err
} }
addr, _ := btcutil.DecodeAddress(changeAddr, &netParams) addr, _ := btcutil.DecodeAddress(changeAddr.Address, &netParams)
pkscript, err := txscript.PayToAddrScript(addr) pkscript, err := txscript.PayToAddrScript(addr)
if err != nil { if err != nil {
@@ -1255,7 +1283,7 @@ func (a *covenantlessArkClient) sendOnchain(
if err != nil { if err != nil {
return "", err return "", err
} }
addr, _ := btcutil.DecodeAddress(changeAddr, &netParams) addr, _ := btcutil.DecodeAddress(changeAddr.Address, &netParams)
pkscript, err := txscript.PayToAddrScript(addr) pkscript, err := txscript.PayToAddrScript(addr)
if err != nil { if err != nil {
@@ -1306,49 +1334,55 @@ func (a *covenantlessArkClient) sendOffchain(
return "", fmt.Errorf("no funds detected") return "", fmt.Errorf("no funds detected")
} }
_, _, aspPubKey, err := common.DecodeAddress(offchainAddrs[0]) expectedAspPubKey := schnorr.SerializePubKey(a.AspPubkey)
if err != nil {
return "", err
}
receiversOutput := make([]client.Output, 0) receiversOutput := make([]client.Output, 0)
sumOfReceivers := uint64(0) sumOfReceivers := uint64(0)
for _, receiver := range receivers { for _, receiver := range receivers {
_, _, aspKey, err := common.DecodeAddress(receiver.To()) rcvAddr, err := common.DecodeAddress(receiver.To())
if err != nil { if err != nil {
return "", fmt.Errorf("invalid receiver address: %s", err) return "", fmt.Errorf("invalid receiver address: %s", err)
} }
if !bytes.Equal( rcvAspPubKey := schnorr.SerializePubKey(rcvAddr.Asp)
aspPubKey.SerializeCompressed(), aspKey.SerializeCompressed(),
) { if !bytes.Equal(expectedAspPubKey, rcvAspPubKey) {
return "", fmt.Errorf("invalid receiver address '%s': must be associated with the connected service provider", receiver.To()) return "", fmt.Errorf("invalid receiver address '%s': expected ASP %s, got %s", receiver.To(), hex.EncodeToString(expectedAspPubKey), hex.EncodeToString(rcvAspPubKey))
} }
if receiver.Amount() < a.Dust { if receiver.Amount() < a.Dust {
return "", fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount(), a.Dust) return "", fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount(), a.Dust)
} }
desc, err := a.offchainAddressToDefaultVtxoDescriptor(receiver.To())
if err != nil {
return "", err
}
receiversOutput = append(receiversOutput, client.Output{ receiversOutput = append(receiversOutput, client.Output{
Descriptor: desc, Address: receiver.To(),
Amount: receiver.Amount(), Amount: receiver.Amount(),
}) })
sumOfReceivers += receiver.Amount() sumOfReceivers += receiver.Amount()
} }
vtxos := make([]client.Vtxo, 0) vtxos := make([]client.DescriptorVtxo, 0)
spendableVtxos, _, err := a.getVtxos(ctx, withExpiryCoinselect)
if err != nil {
return "", err
}
for _, offchainAddr := range offchainAddrs { for _, offchainAddr := range offchainAddrs {
spendableVtxos, _, err := a.getVtxos(ctx, offchainAddr, withExpiryCoinselect) for _, v := range spendableVtxos {
if err != nil { vtxoAddr, err := v.Address(a.AspPubkey, a.Network)
return "", err if err != nil {
return "", err
}
if vtxoAddr == offchainAddr.Address {
vtxos = append(vtxos, client.DescriptorVtxo{
Vtxo: v,
Descriptor: offchainAddr.Descriptor,
})
}
} }
vtxos = append(vtxos, spendableVtxos...)
} }
selectedCoins, changeAmount, err := utils.CoinSelect( selectedCoins, changeAmount, err := utils.CoinSelect(
@@ -1363,15 +1397,9 @@ func (a *covenantlessArkClient) sendOffchain(
if err != nil { if err != nil {
return "", err return "", err
} }
desc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddr)
if err != nil {
return "", err
}
changeReceiver := client.Output{ changeReceiver := client.Output{
Descriptor: desc, Address: offchainAddr.Address,
Amount: changeAmount, Amount: changeAmount,
} }
receiversOutput = append(receiversOutput, changeReceiver) receiversOutput = append(receiversOutput, changeReceiver)
} }
@@ -1428,11 +1456,21 @@ func (a *covenantlessArkClient) addInputs(
return err return err
} }
_, userPubkey, aspPubkey, err := common.DecodeAddress(offchain) vtxoScript, err := tree.ParseVtxoScript(offchain.Descriptor)
if err != nil { if err != nil {
return err return err
} }
var userPubkey, aspPubkey *secp256k1.PublicKey
switch s := vtxoScript.(type) {
case *tree.DefaultVtxoScript:
userPubkey = s.Owner
aspPubkey = s.Asp
default:
return fmt.Errorf("unsupported vtxo script: %T", s)
}
for _, utxo := range utxos { for _, utxo := range utxos {
previousHash, err := chainhash.NewHashFromStr(utxo.Txid) previousHash, err := chainhash.NewHashFromStr(utxo.Txid)
if err != nil { if err != nil {
@@ -1495,7 +1533,7 @@ func (a *covenantlessArkClient) addInputs(
func (a *covenantlessArkClient) handleRoundStream( func (a *covenantlessArkClient) handleRoundStream(
ctx context.Context, ctx context.Context,
paymentID string, paymentID string,
vtxosToSign []client.Vtxo, vtxosToSign []client.DescriptorVtxo,
boardingUtxos []explorer.Utxo, boardingUtxos []explorer.Utxo,
boardingDescriptor string, boardingDescriptor string,
receivers []client.Output, receivers []client.Output,
@@ -1681,7 +1719,7 @@ func (a *covenantlessArkClient) handleRoundSigningNoncesGenerated(
func (a *covenantlessArkClient) handleRoundFinalization( func (a *covenantlessArkClient) handleRoundFinalization(
ctx context.Context, ctx context.Context,
event client.RoundFinalizationEvent, event client.RoundFinalizationEvent,
vtxos []client.Vtxo, vtxos []client.DescriptorVtxo,
boardingUtxos []explorer.Utxo, boardingUtxos []explorer.Utxo,
boardingDescriptor string, boardingDescriptor string,
receivers []client.Output, receivers []client.Output,
@@ -1690,21 +1728,11 @@ func (a *covenantlessArkClient) handleRoundFinalization(
return nil, "", fmt.Errorf("failed to verify congestion tree: %s", err) return nil, "", fmt.Errorf("failed to verify congestion tree: %s", err)
} }
offchainAddr, _, err := a.wallet.NewAddress(ctx, false)
if err != nil {
return nil, "", err
}
_, myPubkey, _, err := common.DecodeAddress(offchainAddr)
if err != nil {
return nil, "", err
}
var forfeits []string var forfeits []string
if len(vtxos) > 0 { if len(vtxos) > 0 {
signedForfeits, err := a.createAndSignForfeits( signedForfeits, err := a.createAndSignForfeits(
ctx, vtxos, event.Connectors, event.MinRelayFeeRate, myPubkey, ctx, vtxos, event.Connectors, event.MinRelayFeeRate,
) )
if err != nil { if err != nil {
return nil, "", err return nil, "", err
@@ -1724,6 +1752,15 @@ func (a *covenantlessArkClient) handleRoundFinalization(
return nil, "", err return nil, "", err
} }
var myPubkey *secp256k1.PublicKey
switch v := boardingVtxoScript.(type) {
case *bitcointree.DefaultVtxoScript:
myPubkey = v.Owner
default:
return nil, "", fmt.Errorf("unsupported boarding descriptor: %s", boardingDescriptor)
}
// add tapscript leaf // add tapscript leaf
forfeitClosure := &bitcointree.MultisigClosure{ forfeitClosure := &bitcointree.MultisigClosure{
Pubkey: myPubkey, Pubkey: myPubkey,
@@ -1869,15 +1906,12 @@ func (a *covenantlessArkClient) validateOffChainReceiver(
) error { ) error {
found := false found := false
receiverVtxoScript, err := bitcointree.ParseVtxoScript(receiver.Descriptor) rcvAddr, err := common.DecodeAddress(receiver.Address)
if err != nil { if err != nil {
return err return err
} }
outputTapKey, _, err := receiverVtxoScript.TapTree() vtxoTapKey := schnorr.SerializePubKey(rcvAddr.VtxoTapKey)
if err != nil {
return err
}
leaves := congestionTree.Leaves() leaves := congestionTree.Leaves()
for _, leaf := range leaves { for _, leaf := range leaves {
@@ -1891,9 +1925,7 @@ func (a *covenantlessArkClient) validateOffChainReceiver(
continue continue
} }
if bytes.Equal( if bytes.Equal(output.PkScript[2:], vtxoTapKey) {
output.PkScript[2:], schnorr.SerializePubKey(outputTapKey),
) {
if output.Value != int64(receiver.Amount) { if output.Value != int64(receiver.Amount) {
continue continue
} }
@@ -1919,10 +1951,9 @@ func (a *covenantlessArkClient) validateOffChainReceiver(
func (a *covenantlessArkClient) createAndSignForfeits( func (a *covenantlessArkClient) createAndSignForfeits(
ctx context.Context, ctx context.Context,
vtxosToSign []client.Vtxo, vtxosToSign []client.DescriptorVtxo,
connectors []string, connectors []string,
feeRate chainfee.SatPerKVByte, feeRate chainfee.SatPerKVByte,
myPubkey *secp256k1.PublicKey,
) ([]string, error) { ) ([]string, error) {
parsedForfeitAddr, err := btcutil.DecodeAddress(a.ForfeitAddress, nil) parsedForfeitAddr, err := btcutil.DecodeAddress(a.ForfeitAddress, nil)
if err != nil { if err != nil {
@@ -1982,9 +2013,21 @@ func (a *covenantlessArkClient) createAndSignForfeits(
Index: vtxo.VOut, Index: vtxo.VOut,
} }
forfeitClosure := &bitcointree.MultisigClosure{ var forfeitClosure bitcointree.Closure
Pubkey: myPubkey,
AspPubkey: a.AspPubkey, switch v := vtxoScript.(type) {
case *bitcointree.DefaultVtxoScript:
forfeitClosure = &bitcointree.MultisigClosure{
Pubkey: v.Owner,
AspPubkey: a.AspPubkey,
}
case *bitcointree.ReversibleVtxoScript:
forfeitClosure = &bitcointree.MultisigClosure{
Pubkey: v.Owner,
AspPubkey: a.AspPubkey,
}
default:
return nil, fmt.Errorf("unsupported vtxo script: %T", vtxoScript)
} }
forfeitLeaf, err := forfeitClosure.Leaf() forfeitLeaf, err := forfeitClosure.Leaf()
@@ -2040,39 +2083,28 @@ func (a *covenantlessArkClient) createAndSignForfeits(
func (a *covenantlessArkClient) coinSelectOnchain( func (a *covenantlessArkClient) coinSelectOnchain(
ctx context.Context, targetAmount uint64, exclude []explorer.Utxo, ctx context.Context, targetAmount uint64, exclude []explorer.Utxo,
) ([]explorer.Utxo, uint64, error) { ) ([]explorer.Utxo, uint64, error) {
offchainAddrs, boardingAddrs, redemptionAddrs, err := a.wallet.GetAddresses(ctx) _, boardingAddrs, redemptionAddrs, err := a.wallet.GetAddresses(ctx)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
_, myPubkey, _, err := common.DecodeAddress(offchainAddrs[0])
if err != nil {
return nil, 0, err
}
myPubkeyStr := hex.EncodeToString(schnorr.SerializePubKey(myPubkey))
descriptorStr := strings.ReplaceAll(
a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
)
boardingScript, err := bitcointree.ParseVtxoScript(descriptorStr)
if err != nil {
return nil, 0, err
}
var boardingTimeout uint
if defaultVtxo, ok := boardingScript.(*bitcointree.DefaultVtxoScript); ok {
boardingTimeout = defaultVtxo.ExitDelay
} else {
return nil, 0, fmt.Errorf("unsupported boarding descriptor: %s", descriptorStr)
}
now := time.Now() now := time.Now()
fetchedUtxos := make([]explorer.Utxo, 0) fetchedUtxos := make([]explorer.Utxo, 0)
for _, addr := range boardingAddrs { for _, addr := range boardingAddrs {
utxos, err := a.explorer.GetUtxos(addr) boardingScript, err := bitcointree.ParseVtxoScript(addr.Descriptor)
if err != nil {
return nil, 0, err
}
var boardingTimeout uint
if defaultVtxo, ok := boardingScript.(*bitcointree.DefaultVtxoScript); ok {
boardingTimeout = defaultVtxo.ExitDelay
} else {
return nil, 0, fmt.Errorf("unsupported boarding descriptor: %s", addr.Descriptor)
}
utxos, err := a.explorer.GetUtxos(addr.Address)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
@@ -2108,7 +2140,7 @@ func (a *covenantlessArkClient) coinSelectOnchain(
fetchedUtxos = make([]explorer.Utxo, 0) fetchedUtxos = make([]explorer.Utxo, 0)
for _, addr := range redemptionAddrs { for _, addr := range redemptionAddrs {
utxos, err := a.explorer.GetUtxos(addr) utxos, err := a.explorer.GetUtxos(addr.Address)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
@@ -2185,11 +2217,11 @@ func (a *covenantlessArkClient) getRedeemBranches(
// Currently, the returned balance is calculated from both spendable and // Currently, the returned balance is calculated from both spendable and
// pending vtxos. // pending vtxos.
func (a *covenantlessArkClient) getOffchainBalance( func (a *covenantlessArkClient) getOffchainBalance(
ctx context.Context, addr string, computeVtxoExpiration bool, ctx context.Context, computeVtxoExpiration bool,
) (uint64, map[int64]uint64, error) { ) (uint64, map[int64]uint64, error) {
amountByExpiration := make(map[int64]uint64, 0) amountByExpiration := make(map[int64]uint64, 0)
vtxos, _, err := a.getVtxos(ctx, addr, computeVtxoExpiration) vtxos, _, err := a.getVtxos(ctx, computeVtxoExpiration)
if err != nil { if err != nil {
return 0, nil, err return 0, nil, err
} }
@@ -2222,13 +2254,13 @@ func (a *covenantlessArkClient) getAllBoardingUtxos(
utxos := []explorer.Utxo{} utxos := []explorer.Utxo{}
ignoreVtxos := make(map[string]struct{}, 0) ignoreVtxos := make(map[string]struct{}, 0)
for _, addr := range boardingAddrs { for _, addr := range boardingAddrs {
txs, err := a.explorer.GetTxs(addr) txs, err := a.explorer.GetTxs(addr.Address)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
for _, tx := range txs { for _, tx := range txs {
for i, vout := range tx.Vout { for i, vout := range tx.Vout {
if vout.Address == addr { if vout.Address == addr.Address {
spentStatuses, err := a.explorer.GetTxOutspends(tx.Txid) spentStatuses, err := a.explorer.GetTxOutspends(tx.Txid)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@@ -2255,39 +2287,29 @@ func (a *covenantlessArkClient) getAllBoardingUtxos(
} }
func (a *covenantlessArkClient) getClaimableBoardingUtxos(ctx context.Context) ([]explorer.Utxo, error) { func (a *covenantlessArkClient) getClaimableBoardingUtxos(ctx context.Context) ([]explorer.Utxo, error) {
offchainAddrs, boardingAddrs, _, err := a.wallet.GetAddresses(ctx) _, boardingAddrs, _, err := a.wallet.GetAddresses(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
_, myPubkey, _, err := common.DecodeAddress(offchainAddrs[0])
if err != nil {
return nil, err
}
myPubkeyStr := hex.EncodeToString(schnorr.SerializePubKey(myPubkey))
descriptorStr := strings.ReplaceAll(
a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
)
boardingScript, err := bitcointree.ParseVtxoScript(descriptorStr)
if err != nil {
return nil, err
}
var boardingTimeout uint
if defaultVtxo, ok := boardingScript.(*bitcointree.DefaultVtxoScript); ok {
boardingTimeout = defaultVtxo.ExitDelay
} else {
return nil, fmt.Errorf("unsupported boarding descriptor: %s", descriptorStr)
}
claimable := make([]explorer.Utxo, 0) claimable := make([]explorer.Utxo, 0)
now := time.Now() now := time.Now()
for _, addr := range boardingAddrs { for _, addr := range boardingAddrs {
boardingUtxos, err := a.explorer.GetUtxos(addr) boardingScript, err := bitcointree.ParseVtxoScript(addr.Descriptor)
if err != nil {
return nil, err
}
var boardingTimeout uint
if defaultVtxo, ok := boardingScript.(*bitcointree.DefaultVtxoScript); ok {
boardingTimeout = defaultVtxo.ExitDelay
} else {
return nil, fmt.Errorf("unsupported boarding descriptor: %s", addr.Descriptor)
}
boardingUtxos, err := a.explorer.GetUtxos(addr.Address)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -2306,7 +2328,7 @@ func (a *covenantlessArkClient) getClaimableBoardingUtxos(ctx context.Context) (
} }
func (a *covenantlessArkClient) getVtxos( func (a *covenantlessArkClient) getVtxos(
ctx context.Context, _ string, computeVtxoExpiration bool, ctx context.Context, computeVtxoExpiration bool,
) ([]client.Vtxo, []client.Vtxo, error) { ) ([]client.Vtxo, []client.Vtxo, error) {
spendableVtxos, _, err := a.ListVtxos(ctx) spendableVtxos, _, err := a.ListVtxos(ctx)
if err != nil { if err != nil {
@@ -2347,14 +2369,10 @@ func (a *covenantlessArkClient) getVtxos(
} }
func (a *covenantlessArkClient) selfTransferAllPendingPayments( func (a *covenantlessArkClient) selfTransferAllPendingPayments(
ctx context.Context, pendingVtxos []client.Vtxo, boardingUtxos []explorer.Utxo, myself client.Output, mypubkey string, ctx context.Context, pendingVtxos []client.DescriptorVtxo, boardingUtxos []explorer.Utxo, myself client.Output, boardingDescriptor string,
) (string, error) { ) (string, error) {
inputs := make([]client.Input, 0, len(pendingVtxos)+len(boardingUtxos)) inputs := make([]client.Input, 0, len(pendingVtxos)+len(boardingUtxos))
boardingDescriptor := strings.ReplaceAll(
a.BoardingDescriptorTemplate, "USER", mypubkey[2:],
)
for _, coin := range pendingVtxos { for _, coin := range pendingVtxos {
inputs = append(inputs, client.Input{ inputs = append(inputs, client.Input{
Outpoint: client.Outpoint{ Outpoint: client.Outpoint{
@@ -2404,42 +2422,6 @@ func (a *covenantlessArkClient) selfTransferAllPendingPayments(
return roundTxid, nil return roundTxid, nil
} }
func (a *covenantlessArkClient) offchainAddressToReversibleVtxoDescriptor(myaddr string, receiveraddr string) (string, error) {
_, receiverPubkey, aspPubkey, err := common.DecodeAddress(receiveraddr)
if err != nil {
return "", err
}
_, userPubKey, _, err := common.DecodeAddress(myaddr)
if err != nil {
return "", err
}
vtxoScript := bitcointree.ReversibleVtxoScript{
Owner: receiverPubkey,
Sender: userPubKey,
Asp: aspPubkey,
ExitDelay: uint(a.UnilateralExitDelay),
}
return vtxoScript.ToDescriptor(), nil
}
func (a *covenantlessArkClient) offchainAddressToDefaultVtxoDescriptor(addr string) (string, error) {
_, userPubKey, aspPubkey, err := common.DecodeAddress(addr)
if err != nil {
return "", err
}
vtxoScript := bitcointree.DefaultVtxoScript{
Owner: userPubKey,
Asp: aspPubkey,
ExitDelay: uint(a.UnilateralExitDelay),
}
return vtxoScript.ToDescriptor(), nil
}
// getBoardingTxs builds the boarding tx history from onchain utxos: // getBoardingTxs builds the boarding tx history from onchain utxos:
// - unspent utxo => pending boarding tx // - unspent utxo => pending boarding tx
// - spent utxo => claimed boarding tx // - spent utxo => claimed boarding tx

View File

@@ -23,10 +23,10 @@ import (
) )
func CoinSelect( func CoinSelect(
vtxos []client.Vtxo, amount, dust uint64, sortByExpirationTime bool, vtxos []client.DescriptorVtxo, amount, dust uint64, sortByExpirationTime bool,
) ([]client.Vtxo, uint64, error) { ) ([]client.DescriptorVtxo, uint64, error) {
selected := make([]client.Vtxo, 0) selected := make([]client.DescriptorVtxo, 0)
notSelected := make([]client.Vtxo, 0) notSelected := make([]client.DescriptorVtxo, 0)
selectedAmount := uint64(0) selectedAmount := uint64(0)
if sortByExpirationTime { if sortByExpirationTime {

View File

@@ -43,41 +43,96 @@ func NewBitcoinWallet(
func (w *bitcoinWallet) GetAddresses( func (w *bitcoinWallet) GetAddresses(
ctx context.Context, ctx context.Context,
) ([]string, []string, []string, error) { ) ([]wallet.DescriptorAddress, []wallet.DescriptorAddress, []wallet.DescriptorAddress, error) {
offchainAddr, boardingAddr, redemptionAddr, err := w.getAddress(ctx) offchainAddr, boardingAddr, err := w.getAddress(ctx)
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
offchainAddrs := []string{offchainAddr} encodedOffchainAddr, err := offchainAddr.Address.Encode()
boardingAddrs := []string{boardingAddr} if err != nil {
redemptionAddrs := []string{redemptionAddr} return nil, nil, nil, err
}
data, err := w.configStore.GetData(ctx)
if err != nil {
return nil, nil, nil, err
}
netParams := utils.ToBitcoinNetwork(data.Network)
redemptionAddr, err := btcutil.NewAddressTaproot(
schnorr.SerializePubKey(offchainAddr.Address.VtxoTapKey),
&netParams,
)
if err != nil {
return nil, nil, nil, err
}
offchainAddrs := []wallet.DescriptorAddress{
{
Descriptor: offchainAddr.Descriptor,
Address: encodedOffchainAddr,
},
}
boardingAddrs := []wallet.DescriptorAddress{
{
Descriptor: boardingAddr.Descriptor,
Address: boardingAddr.Address,
},
}
redemptionAddrs := []wallet.DescriptorAddress{
{
Descriptor: offchainAddr.Descriptor,
Address: redemptionAddr.EncodeAddress(),
},
}
return offchainAddrs, boardingAddrs, redemptionAddrs, nil return offchainAddrs, boardingAddrs, redemptionAddrs, nil
} }
func (w *bitcoinWallet) NewAddress( func (w *bitcoinWallet) NewAddress(
ctx context.Context, _ bool, ctx context.Context, _ bool,
) (string, string, error) { ) (*wallet.DescriptorAddress, *wallet.DescriptorAddress, error) {
offchainAddr, boardingAddr, _, err := w.getAddress(ctx) offchainAddr, boardingAddr, err := w.getAddress(ctx)
if err != nil {
return "", "", err
}
return offchainAddr, boardingAddr, nil
}
func (w *bitcoinWallet) NewAddresses(
ctx context.Context, _ bool, num int,
) ([]string, []string, error) {
offchainAddr, boardingAddr, _, err := w.getAddress(ctx)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
offchainAddrs := make([]string, 0, num) encodedOffchainAddr, err := offchainAddr.Address.Encode()
boardingAddrs := make([]string, 0, num) if err != nil {
return nil, nil, err
}
return &wallet.DescriptorAddress{
Descriptor: offchainAddr.Descriptor,
Address: encodedOffchainAddr,
}, boardingAddr, nil
}
func (w *bitcoinWallet) NewAddresses(
ctx context.Context, _ bool, num int,
) ([]wallet.DescriptorAddress, []wallet.DescriptorAddress, error) {
offchainAddr, boardingAddr, err := w.getAddress(ctx)
if err != nil {
return nil, nil, err
}
offchainAddrs := make([]wallet.DescriptorAddress, 0, num)
boardingAddrs := make([]wallet.DescriptorAddress, 0, num)
for i := 0; i < num; i++ { for i := 0; i < num; i++ {
offchainAddrs = append(offchainAddrs, offchainAddr) encodedOffchainAddr, err := offchainAddr.Address.Encode()
boardingAddrs = append(boardingAddrs, boardingAddr) if err != nil {
return nil, nil, err
}
offchainAddrs = append(offchainAddrs, wallet.DescriptorAddress{
Descriptor: offchainAddr.Descriptor,
Address: encodedOffchainAddr,
})
boardingAddrs = append(boardingAddrs, wallet.DescriptorAddress{
Descriptor: boardingAddr.Descriptor,
Address: boardingAddr.Address,
})
} }
return offchainAddrs, boardingAddrs, nil return offchainAddrs, boardingAddrs, nil
} }
@@ -205,19 +260,21 @@ func (s *bitcoinWallet) SignTransaction(
func (w *bitcoinWallet) getAddress( func (w *bitcoinWallet) getAddress(
ctx context.Context, ctx context.Context,
) (string, string, string, error) { ) (
*struct {
Address common.Address
Descriptor string
},
*wallet.DescriptorAddress,
error,
) {
if w.walletData == nil { if w.walletData == nil {
return "", "", "", fmt.Errorf("wallet not initialized") return nil, nil, fmt.Errorf("wallet not initialized")
} }
data, err := w.configStore.GetData(ctx) data, err := w.configStore.GetData(ctx)
if err != nil { if err != nil {
return "", "", "", err return nil, nil, err
}
offchainAddr, err := common.EncodeAddress(data.Network.Addr, w.walletData.Pubkey, data.AspPubkey)
if err != nil {
return "", "", "", err
} }
netParams := utils.ToBitcoinNetwork(data.Network) netParams := utils.ToBitcoinNetwork(data.Network)
@@ -230,15 +287,13 @@ func (w *bitcoinWallet) getAddress(
vtxoTapKey, _, err := defaultVtxoScript.TapTree() vtxoTapKey, _, err := defaultVtxoScript.TapTree()
if err != nil { if err != nil {
return "", "", "", err return nil, nil, err
} }
redemptionAddr, err := btcutil.NewAddressTaproot( offchainAddress := &common.Address{
schnorr.SerializePubKey(vtxoTapKey), HRP: data.Network.Addr,
&netParams, Asp: data.AspPubkey,
) VtxoTapKey: vtxoTapKey,
if err != nil {
return "", "", "", err
} }
myPubkeyStr := hex.EncodeToString(schnorr.SerializePubKey(w.walletData.Pubkey)) myPubkeyStr := hex.EncodeToString(schnorr.SerializePubKey(w.walletData.Pubkey))
@@ -248,12 +303,12 @@ func (w *bitcoinWallet) getAddress(
boardingVtxoScript, err := bitcointree.ParseVtxoScript(descriptorStr) boardingVtxoScript, err := bitcointree.ParseVtxoScript(descriptorStr)
if err != nil { if err != nil {
return "", "", "", err return nil, nil, err
} }
boardingTapKey, _, err := boardingVtxoScript.TapTree() boardingTapKey, _, err := boardingVtxoScript.TapTree()
if err != nil { if err != nil {
return "", "", "", err return nil, nil, err
} }
boardingAddr, err := btcutil.NewAddressTaproot( boardingAddr, err := btcutil.NewAddressTaproot(
@@ -261,8 +316,18 @@ func (w *bitcoinWallet) getAddress(
&netParams, &netParams,
) )
if err != nil { if err != nil {
return "", "", "", err return nil, nil, err
} }
return offchainAddr, boardingAddr.EncodeAddress(), redemptionAddr.EncodeAddress(), nil return &struct {
Address common.Address
Descriptor string
}{
*offchainAddress, defaultVtxoScript.ToDescriptor(),
},
&wallet.DescriptorAddress{
Descriptor: descriptorStr,
Address: boardingAddr.EncodeAddress(),
},
nil
} }

View File

@@ -44,41 +44,103 @@ func NewLiquidWallet(
func (w *liquidWallet) GetAddresses( func (w *liquidWallet) GetAddresses(
ctx context.Context, ctx context.Context,
) ([]string, []string, []string, error) { ) ([]wallet.DescriptorAddress, []wallet.DescriptorAddress, []wallet.DescriptorAddress, error) {
offchainAddr, boardingAddr, redemptionAddr, err := w.getAddress(ctx) offchainAddr, boardingAddr, err := w.getAddress(ctx)
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
offchainAddrs := []string{offchainAddr} encodedOffchainAddr, err := offchainAddr.Address.Encode()
boardingAddrs := []string{boardingAddr} if err != nil {
redemptionAddrs := []string{redemptionAddr} return nil, nil, nil, err
}
data, err := w.configStore.GetData(ctx)
if err != nil {
return nil, nil, nil, err
}
liquidNet := utils.ToElementsNetwork(data.Network)
vtxoP2TR, err := payment.FromTweakedKey(offchainAddr.Address.VtxoTapKey, &liquidNet, nil)
if err != nil {
return nil, nil, nil, err
}
redemptionAddr, err := vtxoP2TR.TaprootAddress()
if err != nil {
return nil, nil, nil, err
}
offchainAddrs := []wallet.DescriptorAddress{
{
Descriptor: offchainAddr.Descriptor,
Address: encodedOffchainAddr,
},
}
boardingAddrs := []wallet.DescriptorAddress{
{
Descriptor: boardingAddr.Descriptor,
Address: boardingAddr.Address,
},
}
redemptionAddrs := []wallet.DescriptorAddress{
{
Descriptor: offchainAddr.Descriptor,
Address: redemptionAddr,
},
}
return offchainAddrs, boardingAddrs, redemptionAddrs, nil return offchainAddrs, boardingAddrs, redemptionAddrs, nil
} }
func (w *liquidWallet) NewAddress( func (w *liquidWallet) NewAddress(
ctx context.Context, _ bool, ctx context.Context, _ bool,
) (string, string, error) { ) (*wallet.DescriptorAddress, *wallet.DescriptorAddress, error) {
offchainAddr, boardingAddr, _, err := w.getAddress(ctx) offchainAddr, boardingAddr, err := w.getAddress(ctx)
if err != nil {
return "", "", err
}
return offchainAddr, boardingAddr, nil
}
func (w *liquidWallet) NewAddresses(
ctx context.Context, _ bool, num int,
) ([]string, []string, error) {
offchainAddr, boardingAddr, _, err := w.getAddress(ctx)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
offchainAddrs := make([]string, 0, num) encodedOffchainAddr, err := offchainAddr.Address.Encode()
boardingAddrs := make([]string, 0, num) if err != nil {
return nil, nil, err
}
return &wallet.DescriptorAddress{
Descriptor: offchainAddr.Descriptor,
Address: encodedOffchainAddr,
}, &wallet.DescriptorAddress{
Descriptor: boardingAddr.Descriptor,
Address: boardingAddr.Address,
}, nil
}
func (w *liquidWallet) NewAddresses(
ctx context.Context, _ bool, num int,
) ([]wallet.DescriptorAddress, []wallet.DescriptorAddress, error) {
offchainAddr, boardingAddr, err := w.getAddress(ctx)
if err != nil {
return nil, nil, err
}
offchainAddrs := make([]wallet.DescriptorAddress, 0, num)
boardingAddrs := make([]wallet.DescriptorAddress, 0, num)
for i := 0; i < num; i++ { for i := 0; i < num; i++ {
offchainAddrs = append(offchainAddrs, offchainAddr) encodedOffchainAddr, err := offchainAddr.Address.Encode()
boardingAddrs = append(boardingAddrs, boardingAddr) if err != nil {
return nil, nil, err
}
offchainAddrs = append(offchainAddrs, wallet.DescriptorAddress{
Descriptor: offchainAddr.Descriptor,
Address: encodedOffchainAddr,
})
boardingAddrs = append(boardingAddrs, wallet.DescriptorAddress{
Descriptor: boardingAddr.Descriptor,
Address: boardingAddr.Address,
})
} }
return offchainAddrs, boardingAddrs, nil return offchainAddrs, boardingAddrs, nil
} }
@@ -227,21 +289,21 @@ func (s *liquidWallet) SignTransaction(
func (w *liquidWallet) getAddress( func (w *liquidWallet) getAddress(
ctx context.Context, ctx context.Context,
) (string, string, string, error) { ) (
*struct {
Address common.Address
Descriptor string
},
*wallet.DescriptorAddress,
error,
) {
if w.walletData == nil { if w.walletData == nil {
return "", "", "", fmt.Errorf("wallet not initialized") return nil, nil, fmt.Errorf("wallet not initialized")
} }
data, err := w.configStore.GetData(ctx) data, err := w.configStore.GetData(ctx)
if err != nil { if err != nil {
return "", "", "", err return nil, nil, err
}
offchainAddr, err := common.EncodeAddress(
data.Network.Addr, w.walletData.Pubkey, data.AspPubkey,
)
if err != nil {
return "", "", "", err
} }
liquidNet := utils.ToElementsNetwork(data.Network) liquidNet := utils.ToElementsNetwork(data.Network)
@@ -254,17 +316,13 @@ func (w *liquidWallet) getAddress(
vtxoTapKey, _, err := vtxoScript.TapTree() vtxoTapKey, _, err := vtxoScript.TapTree()
if err != nil { if err != nil {
return "", "", "", err return nil, nil, err
} }
vtxoP2tr, err := payment.FromTweakedKey(vtxoTapKey, &liquidNet, nil) offchainAddr := &common.Address{
if err != nil { HRP: data.Network.Addr,
return "", "", "", err Asp: data.AspPubkey,
} VtxoTapKey: vtxoTapKey,
redemptionAddr, err := vtxoP2tr.TaprootAddress()
if err != nil {
return "", "", "", err
} }
myPubkeyStr := hex.EncodeToString(schnorr.SerializePubKey(w.walletData.Pubkey)) myPubkeyStr := hex.EncodeToString(schnorr.SerializePubKey(w.walletData.Pubkey))
@@ -274,23 +332,32 @@ func (w *liquidWallet) getAddress(
onboardingScript, err := tree.ParseVtxoScript(descriptorStr) onboardingScript, err := tree.ParseVtxoScript(descriptorStr)
if err != nil { if err != nil {
return "", "", "", err return nil, nil, err
} }
tapKey, _, err := onboardingScript.TapTree() tapKey, _, err := onboardingScript.TapTree()
if err != nil { if err != nil {
return "", "", "", err return nil, nil, err
} }
p2tr, err := payment.FromTweakedKey(tapKey, &liquidNet, nil) p2tr, err := payment.FromTweakedKey(tapKey, &liquidNet, nil)
if err != nil { if err != nil {
return "", "", "", err return nil, nil, err
} }
boardingAddr, err := p2tr.TaprootAddress() boardingAddr, err := p2tr.TaprootAddress()
if err != nil { if err != nil {
return "", "", "", err return nil, nil, err
} }
return offchainAddr, boardingAddr, redemptionAddr, nil return &struct {
Address common.Address
Descriptor string
}{
Address: *offchainAddr,
Descriptor: vtxoScript.ToDescriptor(),
}, &wallet.DescriptorAddress{
Descriptor: descriptorStr,
Address: boardingAddr,
}, nil
} }

View File

@@ -10,6 +10,11 @@ const (
SingleKeyWallet = "singlekey" SingleKeyWallet = "singlekey"
) )
type DescriptorAddress struct {
Descriptor string
Address string
}
type WalletService interface { type WalletService interface {
GetType() string GetType() string
Create( Create(
@@ -20,13 +25,13 @@ type WalletService interface {
IsLocked() bool IsLocked() bool
GetAddresses( GetAddresses(
ctx context.Context, ctx context.Context,
) (offchainAddresses, boardingAddresses, redemptionAddresses []string, err error) ) (offchainAddresses, boardingAddresses, redemptionAddresses []DescriptorAddress, err error)
NewAddress( NewAddress(
ctx context.Context, change bool, ctx context.Context, change bool,
) (offchainAddr, onchainAddr string, err error) ) (offchainAddr, onchainAddr *DescriptorAddress, err error)
NewAddresses( NewAddresses(
ctx context.Context, change bool, num int, ctx context.Context, change bool, num int,
) (offchainAddresses, onchainAddresses []string, err error) ) (offchainAddresses, onchainAddresses []DescriptorAddress, err error)
SignTransaction( SignTransaction(
ctx context.Context, explorerSvc explorer.Explorer, tx string, ctx context.Context, explorerSvc explorer.Explorer, tx string,
) (signedTx string, err error) ) (signedTx string, err error)

View File

@@ -156,6 +156,7 @@ func (s *covenantService) GetBoardingAddress(ctx context.Context, userPubkey *se
func (s *covenantService) SpendVtxos(ctx context.Context, inputs []ports.Input) (string, error) { func (s *covenantService) SpendVtxos(ctx context.Context, inputs []ports.Input) (string, error) {
vtxosInputs := make([]domain.Vtxo, 0) vtxosInputs := make([]domain.Vtxo, 0)
boardingInputs := make([]ports.BoardingInput, 0) boardingInputs := make([]ports.BoardingInput, 0)
descriptors := make(map[domain.VtxoKey]string)
now := time.Now().Unix() now := time.Now().Unix()
@@ -218,13 +219,14 @@ func (s *covenantService) SpendVtxos(ctx context.Context, inputs []ports.Input)
} }
vtxosInputs = append(vtxosInputs, vtxo) vtxosInputs = append(vtxosInputs, vtxo)
descriptors[vtxo.VtxoKey] = input.Descriptor
} }
payment, err := domain.NewPayment(vtxosInputs) payment, err := domain.NewPayment(vtxosInputs)
if err != nil { if err != nil {
return "", err return "", err
} }
if err := s.paymentRequests.push(*payment, boardingInputs); err != nil { if err := s.paymentRequests.push(*payment, boardingInputs, descriptors); err != nil {
return "", err return "", err
} }
return payment.Id, nil return payment.Id, nil
@@ -324,7 +326,7 @@ func (s *covenantService) CompleteAsyncPayment(ctx context.Context, redeemTx str
return fmt.Errorf("unimplemented") return fmt.Errorf("unimplemented")
} }
func (s *covenantService) CreateAsyncPayment(ctx context.Context, inputs []ports.Input, receivers []domain.Receiver) (string, error) { func (s *covenantService) CreateAsyncPayment(_ context.Context, _ []AsyncPaymentInput, _ []domain.Receiver) (string, error) {
return "", fmt.Errorf("unimplemented") return "", fmt.Errorf("unimplemented")
} }
@@ -345,9 +347,14 @@ func (s *covenantService) SignRoundTx(ctx context.Context, signedRoundTx string)
return nil return nil
} }
func (s *covenantService) ListVtxos(ctx context.Context, pubkey *secp256k1.PublicKey) ([]domain.Vtxo, []domain.Vtxo, error) { func (s *covenantService) ListVtxos(ctx context.Context, address string) ([]domain.Vtxo, []domain.Vtxo, error) {
pk := hex.EncodeToString(pubkey.SerializeCompressed()) decodedAddress, err := common.DecodeAddress(address)
return s.repoManager.Vtxos().GetAllVtxos(ctx, pk) if err != nil {
return nil, nil, fmt.Errorf("failed to decode address: %s", err)
}
pubkey := hex.EncodeToString(schnorr.SerializePubKey(decodedAddress.VtxoTapKey))
return s.repoManager.Vtxos().GetAllVtxos(ctx, pubkey)
} }
func (s *covenantService) GetEventsChannel(ctx context.Context) <-chan domain.RoundEvent { func (s *covenantService) GetEventsChannel(ctx context.Context) <-chan domain.RoundEvent {
@@ -482,7 +489,7 @@ func (s *covenantService) startFinalization() {
if num > paymentsThreshold { if num > paymentsThreshold {
num = paymentsThreshold num = paymentsThreshold
} }
payments, boardingInputs, _ := s.paymentRequests.pop(num) payments, boardingInputs, descriptors, _ := s.paymentRequests.pop(num)
if _, err := round.RegisterPayments(payments); err != nil { if _, err := round.RegisterPayments(payments); err != nil {
round.Fail(fmt.Errorf("failed to register payments: %s", err)) round.Fail(fmt.Errorf("failed to register payments: %s", err))
log.WithError(err).Warn("failed to register payments") log.WithError(err).Warn("failed to register payments")
@@ -517,7 +524,7 @@ func (s *covenantService) startFinalization() {
minRelayFeeRate := s.wallet.MinRelayFeeRate(ctx) minRelayFeeRate := s.wallet.MinRelayFeeRate(ctx)
if needForfeits { if needForfeits {
connectors, forfeitTxs, err = s.builder.BuildForfeitTxs(unsignedPoolTx, payments, minRelayFeeRate) connectors, forfeitTxs, err = s.builder.BuildForfeitTxs(unsignedPoolTx, payments, descriptors, minRelayFeeRate)
if err != nil { if err != nil {
round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err)) round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err))
log.WithError(err).Warn("failed to create connectors and forfeit txs") log.WithError(err).Warn("failed to create connectors and forfeit txs")
@@ -933,53 +940,20 @@ func (s *covenantService) getNewVtxos(round *domain.Round) []domain.Vtxo {
continue // skip fee outputs continue // skip fee outputs
} }
desc := "" vtxoTapKey, err := schnorr.ParsePubKey(out.Script[2:])
found := false if err != nil {
log.WithError(err).Warn("failed to parse vtxo tap key")
for _, p := range round.Payments { continue
if found {
break
}
for _, r := range p.Receivers {
if r.IsOnchain() {
continue
}
vtxoScript, err := tree.ParseVtxoScript(r.Descriptor)
if err != nil {
log.WithError(err).Warn("failed to parse vtxo descriptor")
continue
}
tapKey, _, err := vtxoScript.TapTree()
if err != nil {
log.WithError(err).Warn("failed to compute vtxo tap key")
continue
}
script, err := common.P2TRScript(tapKey)
if err != nil {
log.WithError(err).Warn("failed to create vtxo scriptpubkey")
continue
}
if bytes.Equal(script, out.Script) {
found = true
desc = r.Descriptor
break
}
}
} }
if found { vtxoPubkey := hex.EncodeToString(schnorr.SerializePubKey(vtxoTapKey))
vtxos = append(vtxos, domain.Vtxo{
VtxoKey: domain.VtxoKey{Txid: node.Txid, VOut: uint32(i)}, vtxos = append(vtxos, domain.Vtxo{
Receiver: domain.Receiver{Descriptor: desc, Amount: uint64(out.Value)}, VtxoKey: domain.VtxoKey{Txid: node.Txid, VOut: uint32(i)},
RoundTxid: round.Txid, Pubkey: vtxoPubkey,
}) Amount: uint64(out.Value),
break RoundTxid: round.Txid,
} })
} }
} }
return vtxos return vtxos
@@ -1050,17 +1024,17 @@ func (s *covenantService) restoreWatchingVtxos() error {
func (s *covenantService) extractVtxosScripts(vtxos []domain.Vtxo) ([]string, error) { func (s *covenantService) extractVtxosScripts(vtxos []domain.Vtxo) ([]string, error) {
indexedScripts := make(map[string]struct{}) indexedScripts := make(map[string]struct{})
for _, vtxo := range vtxos { for _, vtxo := range vtxos {
vtxoScript, err := tree.ParseVtxoScript(vtxo.Receiver.Descriptor) vtxoTapKeyBytes, err := hex.DecodeString(vtxo.Pubkey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
tapKey, _, err := vtxoScript.TapTree() vtxoTapKey, err := schnorr.ParsePubKey(vtxoTapKeyBytes)
if err != nil { if err != nil {
return nil, err return nil, err
} }
script, err := common.P2TRScript(tapKey) script, err := common.P2TRScript(vtxoTapKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -19,6 +19,7 @@ import (
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
@@ -177,37 +178,28 @@ func (s *covenantlessService) CompleteAsyncPayment(
// verify that the vtxo is spendable // verify that the vtxo is spendable
vtxo, err := vtxoRepo.GetVtxos(ctx, []domain.VtxoKey{{Txid: vtxoOutpoint.Hash.String(), VOut: vtxoOutpoint.Index}}) vtxos, err := vtxoRepo.GetVtxos(ctx, []domain.VtxoKey{{Txid: vtxoOutpoint.Hash.String(), VOut: vtxoOutpoint.Index}})
if err != nil { if err != nil {
return fmt.Errorf("failed to get vtxo: %s", err) return fmt.Errorf("failed to get vtxo: %s", err)
} }
if len(vtxo) == 0 { if len(vtxos) == 0 {
return fmt.Errorf("vtxo not found") return fmt.Errorf("vtxo not found")
} }
if vtxo[0].Spent { vtxo := vtxos[0]
if vtxo.Spent {
return fmt.Errorf("vtxo already spent") return fmt.Errorf("vtxo already spent")
} }
if vtxo[0].Redeemed { if vtxo.Redeemed {
return fmt.Errorf("vtxo already redeemed") return fmt.Errorf("vtxo already redeemed")
} }
if vtxo[0].Swept { if vtxo.Swept {
return fmt.Errorf("vtxo already swept") return fmt.Errorf("vtxo already swept")
} }
vtxoScript, err := bitcointree.ParseVtxoScript(vtxo[0].Descriptor)
if err != nil {
return fmt.Errorf("failed to parse vtxo script: %s", err)
}
vtxoTapKey, _, err := vtxoScript.TapTree()
if err != nil {
return fmt.Errorf("failed to get taproot key: %s", err)
}
// verify that the user signs a forfeit closure // verify that the user signs a forfeit closure
var userPubKey *secp256k1.PublicKey var userPubKey *secp256k1.PublicKey
@@ -228,6 +220,16 @@ func (s *covenantlessService) CompleteAsyncPayment(
return fmt.Errorf("redeem transaction is not signed") return fmt.Errorf("redeem transaction is not signed")
} }
vtxoPublicKeyBytes, err := hex.DecodeString(vtxo.Pubkey)
if err != nil {
return fmt.Errorf("failed to decode vtxo pubkey: %s", err)
}
vtxoTapKey, err := schnorr.ParsePubKey(vtxoPublicKeyBytes)
if err != nil {
return fmt.Errorf("failed to parse vtxo pubkey: %s", err)
}
// verify witness utxo // verify witness utxo
pkscript, err := common.P2TRScript(vtxoTapKey) pkscript, err := common.P2TRScript(vtxoTapKey)
if err != nil { if err != nil {
@@ -238,7 +240,7 @@ func (s *covenantlessService) CompleteAsyncPayment(
return fmt.Errorf("witness utxo script mismatch") return fmt.Errorf("witness utxo script mismatch")
} }
if input.WitnessUtxo.Value != int64(vtxo[0].Amount) { if input.WitnessUtxo.Value != int64(vtxo.Amount) {
return fmt.Errorf("witness utxo value mismatch") return fmt.Errorf("witness utxo value mismatch")
} }
} }
@@ -260,19 +262,23 @@ func (s *covenantlessService) CompleteAsyncPayment(
vtxos := make([]domain.Vtxo, 0, len(asyncPayData.receivers)) vtxos := make([]domain.Vtxo, 0, len(asyncPayData.receivers))
for outIndex, out := range redeemPtx.UnsignedTx.TxOut { for outIndex, out := range redeemPtx.UnsignedTx.TxOut {
desc := asyncPayData.receivers[outIndex].Descriptor vtxoTapKey, err := schnorr.ParsePubKey(out.PkScript[2:])
_, _, _, _, err := descriptor.ParseReversibleVtxoDescriptor(desc) if err != nil {
isPending := err == nil return fmt.Errorf("failed to parse vtxo taproot key: %s", err)
}
vtxoPubkey := hex.EncodeToString(schnorr.SerializePubKey(vtxoTapKey))
// all pending except the last one
isPending := outIndex < len(asyncPayData.receivers)-1
vtxos = append(vtxos, domain.Vtxo{ vtxos = append(vtxos, domain.Vtxo{
VtxoKey: domain.VtxoKey{ VtxoKey: domain.VtxoKey{
Txid: redeemTxid, Txid: redeemTxid,
VOut: uint32(outIndex), VOut: uint32(outIndex),
}, },
Receiver: domain.Receiver{ Pubkey: vtxoPubkey,
Descriptor: desc, Amount: uint64(out.Value),
Amount: uint64(out.Value),
},
ExpireAt: asyncPayData.expireAt, ExpireAt: asyncPayData.expireAt,
RedeemTx: redeemTx, RedeemTx: redeemTx,
Pending: isPending, Pending: isPending,
@@ -309,11 +315,16 @@ func (s *covenantlessService) CompleteAsyncPayment(
} }
func (s *covenantlessService) CreateAsyncPayment( func (s *covenantlessService) CreateAsyncPayment(
ctx context.Context, inputs []ports.Input, receivers []domain.Receiver, ctx context.Context, inputs []AsyncPaymentInput, receivers []domain.Receiver,
) (string, error) { ) (string, error) {
vtxosKeys := make([]domain.VtxoKey, 0, len(inputs)) vtxosKeys := make([]domain.VtxoKey, 0, len(inputs))
descriptors := make(map[domain.VtxoKey]string)
forfeitLeaves := make(map[domain.VtxoKey]chainhash.Hash)
for _, in := range inputs { for _, in := range inputs {
vtxosKeys = append(vtxosKeys, in.VtxoKey) vtxosKeys = append(vtxosKeys, in.VtxoKey)
descriptors[in.VtxoKey] = in.Descriptor
forfeitLeaves[in.VtxoKey] = in.ForfeitLeafHash
} }
vtxos, err := s.repoManager.Vtxos().GetVtxos(ctx, vtxosKeys) vtxos, err := s.repoManager.Vtxos().GetVtxos(ctx, vtxosKeys)
@@ -351,7 +362,7 @@ func (s *covenantlessService) CreateAsyncPayment(
} }
redeemTx, err := s.builder.BuildAsyncPaymentTransactions( redeemTx, err := s.builder.BuildAsyncPaymentTransactions(
vtxosInputs, s.pubkey, receivers, vtxosInputs, descriptors, forfeitLeaves, receivers,
) )
if err != nil { if err != nil {
return "", fmt.Errorf("failed to build async payment txs: %s", err) return "", fmt.Errorf("failed to build async payment txs: %s", err)
@@ -404,7 +415,7 @@ func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []ports.Inp
now := time.Now().Unix() now := time.Now().Unix()
boardingTxs := make(map[string]wire.MsgTx, 0) // txid -> txhex boardingTxs := make(map[string]wire.MsgTx, 0) // txid -> txhex
descriptors := make(map[domain.VtxoKey]string)
for _, input := range inputs { for _, input := range inputs {
vtxosResult, err := s.repoManager.Vtxos().GetVtxos(ctx, []domain.VtxoKey{input.VtxoKey}) vtxosResult, err := s.repoManager.Vtxos().GetVtxos(ctx, []domain.VtxoKey{input.VtxoKey})
if err != nil || len(vtxosResult) == 0 { if err != nil || len(vtxosResult) == 0 {
@@ -461,6 +472,8 @@ func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []ports.Inp
return "", fmt.Errorf("input %s:%d already swept", vtxo.Txid, vtxo.VOut) return "", fmt.Errorf("input %s:%d already swept", vtxo.Txid, vtxo.VOut)
} }
descriptors[vtxo.VtxoKey] = input.Descriptor
vtxosInputs = append(vtxosInputs, vtxo) vtxosInputs = append(vtxosInputs, vtxo)
} }
@@ -468,7 +481,7 @@ func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []ports.Inp
if err != nil { if err != nil {
return "", err return "", err
} }
if err := s.paymentRequests.push(*payment, boardingInputs); err != nil { if err := s.paymentRequests.push(*payment, boardingInputs, descriptors); err != nil {
return "", err return "", err
} }
return payment.Id, nil return payment.Id, nil
@@ -572,9 +585,14 @@ func (s *covenantlessService) SignRoundTx(ctx context.Context, signedRoundTx str
return nil return nil
} }
func (s *covenantlessService) ListVtxos(ctx context.Context, pubkey *secp256k1.PublicKey) ([]domain.Vtxo, []domain.Vtxo, error) { func (s *covenantlessService) ListVtxos(ctx context.Context, address string) ([]domain.Vtxo, []domain.Vtxo, error) {
pk := hex.EncodeToString(pubkey.SerializeCompressed()) decodedAddress, err := common.DecodeAddress(address)
return s.repoManager.Vtxos().GetAllVtxos(ctx, pk) if err != nil {
return nil, nil, fmt.Errorf("failed to decode address: %s", err)
}
pubkey := hex.EncodeToString(schnorr.SerializePubKey(decodedAddress.VtxoTapKey))
return s.repoManager.Vtxos().GetAllVtxos(ctx, pubkey)
} }
func (s *covenantlessService) GetEventsChannel(ctx context.Context) <-chan domain.RoundEvent { func (s *covenantlessService) GetEventsChannel(ctx context.Context) <-chan domain.RoundEvent {
@@ -771,7 +789,7 @@ func (s *covenantlessService) startFinalization() {
if num > paymentsThreshold { if num > paymentsThreshold {
num = paymentsThreshold num = paymentsThreshold
} }
payments, boardingInputs, cosigners := s.paymentRequests.pop(num) payments, boardingInputs, descriptors, cosigners := s.paymentRequests.pop(num)
if len(payments) > len(cosigners) { if len(payments) > len(cosigners) {
err := fmt.Errorf("missing ephemeral key for payments") err := fmt.Errorf("missing ephemeral key for payments")
round.Fail(fmt.Errorf("round aborted: %s", err)) round.Fail(fmt.Errorf("round aborted: %s", err))
@@ -973,7 +991,7 @@ func (s *covenantlessService) startFinalization() {
minRelayFeeRate := s.wallet.MinRelayFeeRate(ctx) minRelayFeeRate := s.wallet.MinRelayFeeRate(ctx)
if needForfeits { if needForfeits {
connectors, forfeitTxs, err = s.builder.BuildForfeitTxs(unsignedRoundTx, payments, minRelayFeeRate) connectors, forfeitTxs, err = s.builder.BuildForfeitTxs(unsignedRoundTx, payments, descriptors, minRelayFeeRate)
if err != nil { if err != nil {
round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err)) round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err))
log.WithError(err).Warn("failed to create connectors and forfeit txs") log.WithError(err).Warn("failed to create connectors and forfeit txs")
@@ -1332,53 +1350,18 @@ func (s *covenantlessService) getNewVtxos(round *domain.Round) []domain.Vtxo {
continue continue
} }
for i, out := range tx.UnsignedTx.TxOut { for i, out := range tx.UnsignedTx.TxOut {
desc := "" vtxoTapKey, err := schnorr.ParsePubKey(out.PkScript[2:])
found := false if err != nil {
log.WithError(err).Warn("failed to parse vtxo tap key")
for _, p := range round.Payments { continue
if found {
break
}
for _, r := range p.Receivers {
if r.IsOnchain() {
continue
}
vtxoScript, err := bitcointree.ParseVtxoScript(r.Descriptor)
if err != nil {
log.WithError(err).Warn("failed to parse vtxo descriptor")
continue
}
tapKey, _, err := vtxoScript.TapTree()
if err != nil {
log.WithError(err).Warn("failed to compute vtxo tap key")
continue
}
script, err := common.P2TRScript(tapKey)
if err != nil {
log.WithError(err).Warn("failed to create vtxo scriptpubkey")
continue
}
if bytes.Equal(script, out.PkScript) {
found = true
desc = r.Descriptor
break
}
}
} }
if found { vtxos = append(vtxos, domain.Vtxo{
vtxos = append(vtxos, domain.Vtxo{ VtxoKey: domain.VtxoKey{Txid: node.Txid, VOut: uint32(i)},
VtxoKey: domain.VtxoKey{Txid: node.Txid, VOut: uint32(i)}, Pubkey: hex.EncodeToString(schnorr.SerializePubKey(vtxoTapKey)),
Receiver: domain.Receiver{Descriptor: desc, Amount: uint64(out.Value)}, Amount: uint64(out.Value),
RoundTxid: round.Txid, RoundTxid: round.Txid,
}) })
break
}
} }
} }
return vtxos return vtxos
@@ -1450,17 +1433,17 @@ func (s *covenantlessService) extractVtxosScripts(vtxos []domain.Vtxo) ([]string
indexedScripts := make(map[string]struct{}) indexedScripts := make(map[string]struct{})
for _, vtxo := range vtxos { for _, vtxo := range vtxos {
vtxoScript, err := bitcointree.ParseVtxoScript(vtxo.Receiver.Descriptor) vtxoTapKeyBytes, err := hex.DecodeString(vtxo.Pubkey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
tapKey, _, err := vtxoScript.TapTree() vtxoTapKey, err := schnorr.ParsePubKey(vtxoTapKeyBytes)
if err != nil { if err != nil {
return nil, err return nil, err
} }
script, err := common.P2TRScript(tapKey) script, err := common.P2TRScript(vtxoTapKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -5,6 +5,7 @@ import (
"github.com/ark-network/ark/server/internal/core/domain" "github.com/ark-network/ark/server/internal/core/domain"
"github.com/ark-network/ark/server/internal/core/ports" "github.com/ark-network/ark/server/internal/core/ports"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
) )
@@ -12,6 +13,11 @@ var (
paymentsThreshold = int64(128) paymentsThreshold = int64(128)
) )
type AsyncPaymentInput struct {
ports.Input
ForfeitLeafHash chainhash.Hash
}
type Service interface { type Service interface {
Start() error Start() error
Stop() Stop()
@@ -27,12 +33,12 @@ type Service interface {
ctx context.Context, paymentId string, ctx context.Context, paymentId string,
) (lastEvent domain.RoundEvent, err error) ) (lastEvent domain.RoundEvent, err error)
ListVtxos( ListVtxos(
ctx context.Context, pubkey *secp256k1.PublicKey, ctx context.Context, address string,
) (spendableVtxos, spentVtxos []domain.Vtxo, err error) ) (spendableVtxos, spentVtxos []domain.Vtxo, err error)
GetInfo(ctx context.Context) (*ServiceInfo, error) GetInfo(ctx context.Context) (*ServiceInfo, error)
// Async payments // Async payments
CreateAsyncPayment( CreateAsyncPayment(
ctx context.Context, inputs []ports.Input, receivers []domain.Receiver, ctx context.Context, inputs []AsyncPaymentInput, receivers []domain.Receiver,
) (string, error) ) (string, error)
CompleteAsyncPayment( CompleteAsyncPayment(
ctx context.Context, redeemTx string, ctx context.Context, redeemTx string,

View File

@@ -24,13 +24,14 @@ type timedPayment struct {
type paymentsMap struct { type paymentsMap struct {
lock *sync.RWMutex lock *sync.RWMutex
payments map[string]*timedPayment payments map[string]*timedPayment
descriptors map[domain.VtxoKey]string
ephemeralKeys map[string]*secp256k1.PublicKey ephemeralKeys map[string]*secp256k1.PublicKey
} }
func newPaymentsMap() *paymentsMap { func newPaymentsMap() *paymentsMap {
paymentsById := make(map[string]*timedPayment) paymentsById := make(map[string]*timedPayment)
lock := &sync.RWMutex{} lock := &sync.RWMutex{}
return &paymentsMap{lock, paymentsById, make(map[string]*secp256k1.PublicKey)} return &paymentsMap{lock, paymentsById, make(map[domain.VtxoKey]string), make(map[string]*secp256k1.PublicKey)}
} }
func (m *paymentsMap) len() int64 { func (m *paymentsMap) len() int64 {
@@ -58,7 +59,11 @@ func (m *paymentsMap) delete(id string) error {
return nil return nil
} }
func (m *paymentsMap) push(payment domain.Payment, boardingInputs []ports.BoardingInput) error { func (m *paymentsMap) push(
payment domain.Payment,
boardingInputs []ports.BoardingInput,
descriptors map[domain.VtxoKey]string,
) error {
m.lock.Lock() m.lock.Lock()
defer m.lock.Unlock() defer m.lock.Unlock()
@@ -86,6 +91,10 @@ func (m *paymentsMap) push(payment domain.Payment, boardingInputs []ports.Boardi
} }
} }
for key, desc := range descriptors {
m.descriptors[key] = desc
}
m.payments[payment.Id] = &timedPayment{payment, boardingInputs, time.Now(), time.Time{}} m.payments[payment.Id] = &timedPayment{payment, boardingInputs, time.Now(), time.Time{}}
return nil return nil
} }
@@ -102,7 +111,7 @@ func (m *paymentsMap) pushEphemeralKey(paymentId string, pubkey *secp256k1.Publi
return nil return nil
} }
func (m *paymentsMap) pop(num int64) ([]domain.Payment, []ports.BoardingInput, []*secp256k1.PublicKey) { func (m *paymentsMap) pop(num int64) ([]domain.Payment, []ports.BoardingInput, map[domain.VtxoKey]string, []*secp256k1.PublicKey) {
m.lock.Lock() m.lock.Lock()
defer m.lock.Unlock() defer m.lock.Unlock()
@@ -129,6 +138,7 @@ func (m *paymentsMap) pop(num int64) ([]domain.Payment, []ports.BoardingInput, [
payments := make([]domain.Payment, 0, num) payments := make([]domain.Payment, 0, num)
boardingInputs := make([]ports.BoardingInput, 0) boardingInputs := make([]ports.BoardingInput, 0)
cosigners := make([]*secp256k1.PublicKey, 0, num) cosigners := make([]*secp256k1.PublicKey, 0, num)
descriptors := make(map[domain.VtxoKey]string)
for _, p := range paymentsByTime[:num] { for _, p := range paymentsByTime[:num] {
boardingInputs = append(boardingInputs, p.boardingInputs...) boardingInputs = append(boardingInputs, p.boardingInputs...)
payments = append(payments, p.Payment) payments = append(payments, p.Payment)
@@ -136,9 +146,15 @@ func (m *paymentsMap) pop(num int64) ([]domain.Payment, []ports.BoardingInput, [
cosigners = append(cosigners, pubkey) cosigners = append(cosigners, pubkey)
delete(m.ephemeralKeys, p.Payment.Id) delete(m.ephemeralKeys, p.Payment.Id)
} }
for _, input := range payments {
for _, vtxo := range input.Inputs {
descriptors[vtxo.VtxoKey] = m.descriptors[vtxo.VtxoKey]
delete(m.descriptors, vtxo.VtxoKey)
}
}
delete(m.payments, p.Id) delete(m.payments, p.Id)
} }
return payments, boardingInputs, cosigners return payments, boardingInputs, descriptors, cosigners
} }
func (m *paymentsMap) update(payment domain.Payment) error { func (m *paymentsMap) update(payment domain.Payment) error {

View File

@@ -68,9 +68,12 @@ func (p Payment) validate(ignoreOuts bool) error {
return fmt.Errorf("missing outputs") return fmt.Errorf("missing outputs")
} }
for _, r := range p.Receivers { for _, r := range p.Receivers {
if len(r.OnchainAddress) <= 0 && len(r.Descriptor) <= 0 { if len(r.OnchainAddress) <= 0 && len(r.Pubkey) <= 0 {
return fmt.Errorf("missing receiver destination") return fmt.Errorf("missing receiver destination")
} }
if r.Amount == 0 {
return fmt.Errorf("missing receiver amount")
}
} }
return nil return nil
} }
@@ -80,6 +83,10 @@ type VtxoKey struct {
VOut uint32 VOut uint32
} }
func (k VtxoKey) String() string {
return fmt.Sprintf("%s:%d", k.Txid, k.VOut)
}
func (k VtxoKey) Hash() string { func (k VtxoKey) Hash() string {
calcHash := func(buf []byte, hasher hash.Hash) []byte { calcHash := func(buf []byte, hasher hash.Hash) []byte {
_, _ = hasher.Write(buf) _, _ = hasher.Write(buf)
@@ -96,9 +103,9 @@ func (k VtxoKey) Hash() string {
} }
type Receiver struct { type Receiver struct {
Descriptor string
Amount uint64 Amount uint64
OnchainAddress string OnchainAddress string // onchain
Pubkey string // offchain
} }
func (r Receiver) IsOnchain() bool { func (r Receiver) IsOnchain() bool {
@@ -107,7 +114,8 @@ func (r Receiver) IsOnchain() bool {
type Vtxo struct { type Vtxo struct {
VtxoKey VtxoKey
Receiver Amount uint64
Pubkey string
RoundTxid string RoundTxid string
SpentBy string // round txid or async redeem txid SpentBy string // round txid or async redeem txid
Spent bool Spent bool

View File

@@ -1,22 +1,14 @@
package domain_test package domain_test
import ( import (
"fmt"
"testing" "testing"
"github.com/ark-network/ark/common/descriptor"
"github.com/ark-network/ark/server/internal/core/domain" "github.com/ark-network/ark/server/internal/core/domain"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
var desc = fmt.Sprintf( // x-only pubkey
descriptor.DefaultVtxoDescriptorTemplate, const pubkey = "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967"
"030000000000000000000000000000000000000000000000000000000000000001",
"0000000000000000000000000000000000000000000000000000000000000001",
"0000000000000000000000000000000000000000000000000000000000000001",
512,
"0000000000000000000000000000000000000000000000000000000000000001",
)
var inputs = []domain.Vtxo{ var inputs = []domain.Vtxo{
{ {
@@ -24,10 +16,8 @@ var inputs = []domain.Vtxo{
Txid: "0000000000000000000000000000000000000000000000000000000000000000", Txid: "0000000000000000000000000000000000000000000000000000000000000000",
VOut: 0, VOut: 0,
}, },
Receiver: domain.Receiver{ Pubkey: pubkey,
Descriptor: desc, Amount: 1000,
Amount: 1000,
},
}, },
} }
@@ -51,12 +41,12 @@ func TestPayment(t *testing.T) {
err = payment.AddReceivers([]domain.Receiver{ err = payment.AddReceivers([]domain.Receiver{
{ {
Descriptor: desc, Pubkey: pubkey,
Amount: 450, Amount: 450,
}, },
{ {
Descriptor: desc, Pubkey: pubkey,
Amount: 550, Amount: 550,
}, },
}) })
require.NoError(t, err) require.NoError(t, err)

View File

@@ -20,24 +20,22 @@ var (
Txid: txid, Txid: txid,
VOut: 0, VOut: 0,
}, },
Receiver: domain.Receiver{ Pubkey: pubkey,
Descriptor: desc, Amount: 2000,
Amount: 2000,
},
}, },
}, },
Receivers: []domain.Receiver{ Receivers: []domain.Receiver{
{ {
Descriptor: desc, Pubkey: pubkey,
Amount: 700, Amount: 700,
}, },
{ {
Descriptor: desc, Pubkey: pubkey,
Amount: 700, Amount: 700,
}, },
{ {
Descriptor: desc, Pubkey: pubkey,
Amount: 600, Amount: 600,
}, },
}, },
}, },
@@ -49,25 +47,21 @@ var (
Txid: txid, Txid: txid,
VOut: 0, VOut: 0,
}, },
Receiver: domain.Receiver{ Pubkey: pubkey,
Descriptor: desc, Amount: 1000,
Amount: 1000,
},
}, },
{ {
VtxoKey: domain.VtxoKey{ VtxoKey: domain.VtxoKey{
Txid: txid, Txid: txid,
VOut: 0, VOut: 0,
}, },
Receiver: domain.Receiver{ Pubkey: pubkey,
Descriptor: desc, Amount: 1000,
Amount: 1000,
},
}, },
}, },
Receivers: []domain.Receiver{{ Receivers: []domain.Receiver{{
Descriptor: desc, Pubkey: pubkey,
Amount: 2000, Amount: 2000,
}}, }},
}, },
} }

View File

@@ -32,7 +32,12 @@ type TxBuilder interface {
aspPubkey *secp256k1.PublicKey, payments []domain.Payment, boardingInputs []BoardingInput, sweptRounds []domain.Round, aspPubkey *secp256k1.PublicKey, payments []domain.Payment, boardingInputs []BoardingInput, sweptRounds []domain.Round,
cosigners ...*secp256k1.PublicKey, cosigners ...*secp256k1.PublicKey,
) (roundTx string, congestionTree tree.CongestionTree, connectorAddress string, err error) ) (roundTx string, congestionTree tree.CongestionTree, connectorAddress string, err error)
BuildForfeitTxs(poolTx string, payments []domain.Payment, minRelayFeeRate chainfee.SatPerKVByte) (connectors []string, forfeitTxs []string, err error) BuildForfeitTxs(
roundTx string,
payments []domain.Payment,
descriptors map[domain.VtxoKey]string,
minRelayFeeRate chainfee.SatPerKVByte,
) (connectors []string, forfeitTxs []string, err error)
BuildSweepTx(inputs []SweepInput) (signedSweepTx string, err error) BuildSweepTx(inputs []SweepInput) (signedSweepTx string, err error)
GetSweepInput(node tree.Node) (lifetime int64, sweepInput SweepInput, err error) GetSweepInput(node tree.Node) (lifetime int64, sweepInput SweepInput, err error)
FinalizeAndExtract(tx string) (txhex string, err error) FinalizeAndExtract(tx string) (txhex string, err error)
@@ -41,7 +46,9 @@ type TxBuilder interface {
FindLeaves(congestionTree tree.CongestionTree, fromtxid string, vout uint32) (leaves []tree.Node, err error) FindLeaves(congestionTree tree.CongestionTree, fromtxid string, vout uint32) (leaves []tree.Node, err error)
BuildAsyncPaymentTransactions( BuildAsyncPaymentTransactions(
vtxosToSpend []domain.Vtxo, vtxosToSpend []domain.Vtxo,
aspPubKey *secp256k1.PublicKey, receivers []domain.Receiver, descriptors map[domain.VtxoKey]string,
forfeitsLeaves map[domain.VtxoKey]chainhash.Hash,
receivers []domain.Receiver,
) (string, error) ) (string, error)
VerifyAndCombinePartialTx(dest string, src string) (string, error) VerifyAndCombinePartialTx(dest string, src string) (string, error)
GetTxID(tx string) (string, error) GetTxID(tx string) (string, error)

View File

@@ -4,7 +4,6 @@ import (
"context" "context"
"fmt" "fmt"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"github.com/ark-network/ark/server/internal/core/domain" "github.com/ark-network/ark/server/internal/core/domain"
@@ -101,13 +100,7 @@ func (r *vtxoRepository) GetAllVtxos(
) ([]domain.Vtxo, []domain.Vtxo, error) { ) ([]domain.Vtxo, []domain.Vtxo, error) {
query := badgerhold.Where("Redeemed").Eq(false) query := badgerhold.Where("Redeemed").Eq(false)
if len(pubkey) > 0 { if len(pubkey) > 0 {
if len(pubkey) == 66 { query = query.And("Pubkey").Eq(pubkey)
pubkey = pubkey[2:]
}
query = query.And("Descriptor").RegExp(
regexp.MustCompile(fmt.Sprintf(".*%s.*", pubkey)),
)
} }
vtxos, err := r.findVtxos(ctx, query) vtxos, err := r.findVtxos(ctx, query)
if err != nil { if err != nil {

View File

@@ -4,14 +4,12 @@ import (
"context" "context"
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"fmt"
"os" "os"
"reflect" "reflect"
"sort" "sort"
"testing" "testing"
"time" "time"
"github.com/ark-network/ark/common/descriptor"
"github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/server/internal/core/domain" "github.com/ark-network/ark/server/internal/core/domain"
"github.com/ark-network/ark/server/internal/core/ports" "github.com/ark-network/ark/server/internal/core/ports"
@@ -24,26 +22,8 @@ import (
const ( const (
emptyPtx = "cHNldP8BAgQCAAAAAQQBAAEFAQABBgEDAfsEAgAAAAA=" emptyPtx = "cHNldP8BAgQCAAAAAQQBAAEFAQABBgEDAfsEAgAAAAA="
emptyTx = "0200000000000000000000" emptyTx = "0200000000000000000000"
pubkey1 = "00000000000000000000000000000000000000000000000000000000000000001" pubkey = "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967"
pubkey2 = "00000000000000000000000000000000000000000000000000000000000000002" pubkey2 = "33ffb3dee353b1a9ebe4ced64b946238d0a4ac364f275d771da6ad2445d07ae0"
)
var desc1 = fmt.Sprintf(
descriptor.DefaultVtxoDescriptorTemplate,
randomString(66),
pubkey1,
pubkey1,
512,
pubkey1,
)
var desc2 = fmt.Sprintf(
descriptor.DefaultVtxoDescriptorTemplate,
randomString(66),
pubkey2,
pubkey2,
512,
pubkey2,
) )
var congestionTree = [][]tree.Node{ var congestionTree = [][]tree.Node{
@@ -270,15 +250,13 @@ func testRoundRepository(t *testing.T, svc ports.RepoManager) {
}, },
RoundTxid: randomString(32), RoundTxid: randomString(32),
ExpireAt: 7980322, ExpireAt: 7980322,
Receiver: domain.Receiver{ Pubkey: randomString(32),
Descriptor: randomString(120), Amount: 300,
Amount: 300,
},
}, },
}, },
Receivers: []domain.Receiver{{ Receivers: []domain.Receiver{{
Descriptor: randomString(120), Pubkey: randomString(32),
Amount: 300, Amount: 300,
}}, }},
}, },
{ {
@@ -292,20 +270,18 @@ func testRoundRepository(t *testing.T, svc ports.RepoManager) {
}, },
RoundTxid: randomString(32), RoundTxid: randomString(32),
ExpireAt: 7980322, ExpireAt: 7980322,
Receiver: domain.Receiver{ Pubkey: randomString(32),
Descriptor: randomString(120), Amount: 600,
Amount: 600,
},
}, },
}, },
Receivers: []domain.Receiver{ Receivers: []domain.Receiver{
{ {
Descriptor: randomString(120), Pubkey: randomString(32),
Amount: 400, Amount: 400,
}, },
{ {
Descriptor: randomString(120), Pubkey: randomString(32),
Amount: 200, Amount: 200,
}, },
}, },
}, },
@@ -370,20 +346,16 @@ func testVtxoRepository(t *testing.T, svc ports.RepoManager) {
Txid: randomString(32), Txid: randomString(32),
VOut: 0, VOut: 0,
}, },
Receiver: domain.Receiver{ Pubkey: pubkey,
Descriptor: desc1, Amount: 1000,
Amount: 1000,
},
}, },
{ {
VtxoKey: domain.VtxoKey{ VtxoKey: domain.VtxoKey{
Txid: randomString(32), Txid: randomString(32),
VOut: 1, VOut: 1,
}, },
Receiver: domain.Receiver{ Pubkey: pubkey,
Descriptor: desc1, Amount: 2000,
Amount: 2000,
},
}, },
} }
newVtxos := append(userVtxos, domain.Vtxo{ newVtxos := append(userVtxos, domain.Vtxo{
@@ -391,10 +363,8 @@ func testVtxoRepository(t *testing.T, svc ports.RepoManager) {
Txid: randomString(32), Txid: randomString(32),
VOut: 1, VOut: 1,
}, },
Receiver: domain.Receiver{ Pubkey: pubkey2,
Descriptor: desc2, Amount: 2000,
Amount: 2000,
},
}) })
vtxoKeys := make([]domain.VtxoKey, 0, len(userVtxos)) vtxoKeys := make([]domain.VtxoKey, 0, len(userVtxos))
@@ -406,7 +376,7 @@ func testVtxoRepository(t *testing.T, svc ports.RepoManager) {
require.Error(t, err) require.Error(t, err)
require.Empty(t, vtxos) require.Empty(t, vtxos)
spendableVtxos, spentVtxos, err := svc.Vtxos().GetAllVtxos(ctx, pubkey1) spendableVtxos, spentVtxos, err := svc.Vtxos().GetAllVtxos(ctx, pubkey)
require.NoError(t, err) require.NoError(t, err)
require.Empty(t, spendableVtxos) require.Empty(t, spendableVtxos)
require.Empty(t, spentVtxos) require.Empty(t, spentVtxos)
@@ -423,7 +393,7 @@ func testVtxoRepository(t *testing.T, svc ports.RepoManager) {
require.NoError(t, err) require.NoError(t, err)
require.Exactly(t, userVtxos, vtxos) require.Exactly(t, userVtxos, vtxos)
spendableVtxos, spentVtxos, err = svc.Vtxos().GetAllVtxos(ctx, pubkey1) spendableVtxos, spentVtxos, err = svc.Vtxos().GetAllVtxos(ctx, pubkey)
require.NoError(t, err) require.NoError(t, err)
sortedVtxos := sortVtxos(userVtxos) sortedVtxos := sortVtxos(userVtxos)
@@ -449,7 +419,7 @@ func testVtxoRepository(t *testing.T, svc ports.RepoManager) {
require.True(t, v.Spent) require.True(t, v.Spent)
} }
spendableVtxos, spentVtxos, err = svc.Vtxos().GetAllVtxos(ctx, pubkey1) spendableVtxos, spentVtxos, err = svc.Vtxos().GetAllVtxos(ctx, pubkey)
require.NoError(t, err) require.NoError(t, err)
require.Exactly(t, vtxos[1:], spendableVtxos) require.Exactly(t, vtxos[1:], spendableVtxos)
require.Len(t, spentVtxos, len(vtxoKeys[:1])) require.Len(t, spentVtxos, len(vtxoKeys[:1]))

View File

@@ -21,11 +21,11 @@ CREATE TABLE IF NOT EXISTS payment (
CREATE TABLE IF NOT EXISTS receiver ( CREATE TABLE IF NOT EXISTS receiver (
payment_id TEXT NOT NULL, payment_id TEXT NOT NULL,
pubkey TEXT NOT NULL, pubkey TEXT,
onchain_address TEXT,
amount INTEGER NOT NULL, amount INTEGER NOT NULL,
onchain_address TEXT NOT NULL,
FOREIGN KEY (payment_id) REFERENCES payment(id), FOREIGN KEY (payment_id) REFERENCES payment(id),
PRIMARY KEY (payment_id, pubkey) PRIMARY KEY (payment_id, pubkey, onchain_address)
); );
CREATE TABLE IF NOT EXISTS tx ( CREATE TABLE IF NOT EXISTS tx (
@@ -54,6 +54,7 @@ CREATE TABLE IF NOT EXISTS vtxo (
expire_at INTEGER NOT NULL, expire_at INTEGER NOT NULL,
payment_id TEXT, payment_id TEXT,
redeem_tx TEXT, redeem_tx TEXT,
pending BOOLEAN NOT NULL,
PRIMARY KEY (txid, vout), PRIMARY KEY (txid, vout),
FOREIGN KEY (payment_id) REFERENCES payment(id) FOREIGN KEY (payment_id) REFERENCES payment(id)
); );

View File

@@ -1,17 +0,0 @@
CREATE TABLE IF NOT EXISTS old_receiver (
payment_id TEXT NOT NULL,
pubkey TEXT NOT NULL,
amount INTEGER NOT NULL,
onchain_address TEXT NOT NULL,
FOREIGN KEY (payment_id) REFERENCES payment(id),
PRIMARY KEY (payment_id, pubkey)
);
INSERT INTO old_receiver SELECT * FROM receiver;
DROP TABLE receiver;
ALTER TABLE old_receiver RENAME TO receiver;
ALTER TABLE vtxo DROP COLUMN descriptor;
ALTER TABLE vtxo ADD COLUMN pubkey TEXT NOT NULL;

View File

@@ -1,28 +0,0 @@
CREATE TABLE IF NOT EXISTS new_receiver (
payment_id TEXT NOT NULL,
descriptor TEXT NOT NULL,
amount INTEGER NOT NULL,
onchain_address TEXT NOT NULL,
FOREIGN KEY (payment_id) REFERENCES payment(id),
PRIMARY KEY (payment_id, descriptor)
);
INSERT INTO new_receiver SELECT * FROM receiver;
DROP VIEW payment_vtxo_vw;
DROP VIEW payment_receiver_vw;
DROP TABLE receiver;
ALTER TABLE new_receiver RENAME TO receiver;
ALTER TABLE vtxo ADD COLUMN descriptor TEXT;
ALTER TABLE vtxo DROP COLUMN pubkey;
CREATE VIEW payment_vtxo_vw AS SELECT vtxo.*
FROM payment
LEFT OUTER JOIN vtxo
ON payment.id=vtxo.payment_id;
CREATE VIEW payment_receiver_vw AS SELECT receiver.*
FROM payment
LEFT OUTER JOIN receiver
ON payment.id=receiver.payment_id;

View File

@@ -1 +0,0 @@
ALTER TABLE vtxo DROP COLUMN pending;

View File

@@ -1,14 +0,0 @@
ALTER TABLE vtxo ADD COLUMN pending BOOLEAN NOT NULL;
DROP VIEW payment_vtxo_vw;
DROP VIEW payment_receiver_vw;
CREATE VIEW payment_vtxo_vw AS SELECT vtxo.*
FROM payment
LEFT OUTER JOIN vtxo
ON payment.id=vtxo.payment_id;
CREATE VIEW payment_receiver_vw AS SELECT receiver.*
FROM payment
LEFT OUTER JOIN receiver
ON payment.id=receiver.payment_id;

View File

@@ -164,10 +164,16 @@ func (r *roundRepository) AddOrUpdateRound(ctx context.Context, round domain.Rou
if err := querierWithTx.UpsertReceiver( if err := querierWithTx.UpsertReceiver(
ctx, ctx,
queries.UpsertReceiverParams{ queries.UpsertReceiverParams{
PaymentID: payment.Id, PaymentID: payment.Id,
Descriptor: receiver.Descriptor, Amount: int64(receiver.Amount),
Amount: int64(receiver.Amount), Pubkey: sql.NullString{
OnchainAddress: receiver.OnchainAddress, String: receiver.Pubkey,
Valid: len(receiver.Pubkey) > 0,
},
OnchainAddress: sql.NullString{
String: receiver.OnchainAddress,
Valid: len(receiver.OnchainAddress) > 0,
},
}, },
); err != nil { ); err != nil {
return fmt.Errorf("failed to upsert receiver: %w", err) return fmt.Errorf("failed to upsert receiver: %w", err)
@@ -320,8 +326,8 @@ func (r *roundRepository) GetSweptRounds(ctx context.Context) ([]domain.Round, e
func rowToReceiver(row queries.PaymentReceiverVw) domain.Receiver { func rowToReceiver(row queries.PaymentReceiverVw) domain.Receiver {
return domain.Receiver{ return domain.Receiver{
Descriptor: row.Descriptor.String,
Amount: uint64(row.Amount.Int64), Amount: uint64(row.Amount.Int64),
Pubkey: row.Pubkey.String,
OnchainAddress: row.OnchainAddress.String, OnchainAddress: row.OnchainAddress.String,
} }
} }
@@ -413,8 +419,8 @@ func readRoundRows(rows []roundPaymentTxReceiverVtxoRow) ([]*domain.Round, error
found := false found := false
for _, rcv := range payment.Receivers { for _, rcv := range payment.Receivers {
if v.receiver.Descriptor.Valid && v.receiver.Amount.Valid { if (v.receiver.Pubkey.Valid || v.receiver.OnchainAddress.Valid) && v.receiver.Amount.Valid {
if rcv.Descriptor == v.receiver.Descriptor.String && int64(rcv.Amount) == v.receiver.Amount.Int64 { if rcv.Pubkey == v.receiver.Pubkey.String && rcv.OnchainAddress == v.receiver.OnchainAddress.String && int64(rcv.Amount) == v.receiver.Amount.Int64 {
found = true found = true
break break
} }
@@ -469,10 +475,8 @@ func rowToPaymentVtxoVw(row queries.PaymentVtxoVw) domain.Vtxo {
Txid: row.Txid.String, Txid: row.Txid.String,
VOut: uint32(row.Vout.Int64), VOut: uint32(row.Vout.Int64),
}, },
Receiver: domain.Receiver{ Amount: uint64(row.Amount.Int64),
Descriptor: row.Descriptor.String, Pubkey: row.Pubkey.String,
Amount: uint64(row.Amount.Int64),
},
RoundTxid: row.PoolTx.String, RoundTxid: row.PoolTx.String,
SpentBy: row.SpentBy.String, SpentBy: row.SpentBy.String,
Spent: row.Spent.Bool, Spent: row.Spent.Bool,

View File

@@ -15,32 +15,32 @@ type Payment struct {
type PaymentReceiverVw struct { type PaymentReceiverVw struct {
PaymentID sql.NullString PaymentID sql.NullString
Descriptor sql.NullString Pubkey sql.NullString
Amount sql.NullInt64
OnchainAddress sql.NullString OnchainAddress sql.NullString
Amount sql.NullInt64
} }
type PaymentVtxoVw struct { type PaymentVtxoVw struct {
Txid sql.NullString Txid sql.NullString
Vout sql.NullInt64 Vout sql.NullInt64
Amount sql.NullInt64 Pubkey sql.NullString
PoolTx sql.NullString Amount sql.NullInt64
SpentBy sql.NullString PoolTx sql.NullString
Spent sql.NullBool SpentBy sql.NullString
Redeemed sql.NullBool Spent sql.NullBool
Swept sql.NullBool Redeemed sql.NullBool
ExpireAt sql.NullInt64 Swept sql.NullBool
PaymentID sql.NullString ExpireAt sql.NullInt64
RedeemTx sql.NullString PaymentID sql.NullString
Descriptor sql.NullString RedeemTx sql.NullString
Pending sql.NullBool Pending sql.NullBool
} }
type Receiver struct { type Receiver struct {
PaymentID string PaymentID string
Descriptor string Pubkey sql.NullString
OnchainAddress sql.NullString
Amount int64 Amount int64
OnchainAddress string
} }
type Round struct { type Round struct {
@@ -88,17 +88,17 @@ type Tx struct {
} }
type Vtxo struct { type Vtxo struct {
Txid string Txid string
Vout int64 Vout int64
Amount int64 Pubkey string
PoolTx string Amount int64
SpentBy string PoolTx string
Spent bool SpentBy string
Redeemed bool Spent bool
Swept bool Redeemed bool
ExpireAt int64 Swept bool
PaymentID sql.NullString ExpireAt int64
RedeemTx sql.NullString PaymentID sql.NullString
Descriptor sql.NullString RedeemTx sql.NullString
Pending bool Pending bool
} }

View File

@@ -54,7 +54,7 @@ func (q *Queries) MarkVtxoAsSwept(ctx context.Context, arg MarkVtxoAsSweptParams
} }
const selectNotRedeemedVtxos = `-- name: SelectNotRedeemedVtxos :many const selectNotRedeemedVtxos = `-- name: SelectNotRedeemedVtxos :many
SELECT vtxo.txid, vtxo.vout, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, vtxo.descriptor, vtxo.pending FROM vtxo SELECT vtxo.txid, vtxo.vout, vtxo.pubkey, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, vtxo.pending FROM vtxo
WHERE redeemed = false WHERE redeemed = false
` `
@@ -74,6 +74,7 @@ func (q *Queries) SelectNotRedeemedVtxos(ctx context.Context) ([]SelectNotRedeem
if err := rows.Scan( if err := rows.Scan(
&i.Vtxo.Txid, &i.Vtxo.Txid,
&i.Vtxo.Vout, &i.Vtxo.Vout,
&i.Vtxo.Pubkey,
&i.Vtxo.Amount, &i.Vtxo.Amount,
&i.Vtxo.PoolTx, &i.Vtxo.PoolTx,
&i.Vtxo.SpentBy, &i.Vtxo.SpentBy,
@@ -83,7 +84,6 @@ func (q *Queries) SelectNotRedeemedVtxos(ctx context.Context) ([]SelectNotRedeem
&i.Vtxo.ExpireAt, &i.Vtxo.ExpireAt,
&i.Vtxo.PaymentID, &i.Vtxo.PaymentID,
&i.Vtxo.RedeemTx, &i.Vtxo.RedeemTx,
&i.Vtxo.Descriptor,
&i.Vtxo.Pending, &i.Vtxo.Pending,
); err != nil { ); err != nil {
return nil, err return nil, err
@@ -100,16 +100,16 @@ func (q *Queries) SelectNotRedeemedVtxos(ctx context.Context) ([]SelectNotRedeem
} }
const selectNotRedeemedVtxosWithPubkey = `-- name: SelectNotRedeemedVtxosWithPubkey :many const selectNotRedeemedVtxosWithPubkey = `-- name: SelectNotRedeemedVtxosWithPubkey :many
SELECT vtxo.txid, vtxo.vout, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, vtxo.descriptor, vtxo.pending FROM vtxo SELECT vtxo.txid, vtxo.vout, vtxo.pubkey, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, vtxo.pending FROM vtxo
WHERE redeemed = false AND INSTR(descriptor, ?) > 0 WHERE redeemed = false AND pubkey = ?
` `
type SelectNotRedeemedVtxosWithPubkeyRow struct { type SelectNotRedeemedVtxosWithPubkeyRow struct {
Vtxo Vtxo Vtxo Vtxo
} }
func (q *Queries) SelectNotRedeemedVtxosWithPubkey(ctx context.Context, instr string) ([]SelectNotRedeemedVtxosWithPubkeyRow, error) { func (q *Queries) SelectNotRedeemedVtxosWithPubkey(ctx context.Context, pubkey string) ([]SelectNotRedeemedVtxosWithPubkeyRow, error) {
rows, err := q.db.QueryContext(ctx, selectNotRedeemedVtxosWithPubkey, instr) rows, err := q.db.QueryContext(ctx, selectNotRedeemedVtxosWithPubkey, pubkey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -120,6 +120,7 @@ func (q *Queries) SelectNotRedeemedVtxosWithPubkey(ctx context.Context, instr st
if err := rows.Scan( if err := rows.Scan(
&i.Vtxo.Txid, &i.Vtxo.Txid,
&i.Vtxo.Vout, &i.Vtxo.Vout,
&i.Vtxo.Pubkey,
&i.Vtxo.Amount, &i.Vtxo.Amount,
&i.Vtxo.PoolTx, &i.Vtxo.PoolTx,
&i.Vtxo.SpentBy, &i.Vtxo.SpentBy,
@@ -129,7 +130,6 @@ func (q *Queries) SelectNotRedeemedVtxosWithPubkey(ctx context.Context, instr st
&i.Vtxo.ExpireAt, &i.Vtxo.ExpireAt,
&i.Vtxo.PaymentID, &i.Vtxo.PaymentID,
&i.Vtxo.RedeemTx, &i.Vtxo.RedeemTx,
&i.Vtxo.Descriptor,
&i.Vtxo.Pending, &i.Vtxo.Pending,
); err != nil { ); err != nil {
return nil, err return nil, err
@@ -208,8 +208,8 @@ const selectRoundWithRoundId = `-- name: SelectRoundWithRoundId :many
SELECT round.id, round.starting_timestamp, round.ending_timestamp, round.ended, round.failed, round.stage_code, round.txid, round.unsigned_tx, round.connector_address, round.dust_amount, round.version, round.swept, SELECT round.id, round.starting_timestamp, round.ending_timestamp, round.ended, round.failed, round.stage_code, round.txid, round.unsigned_tx, round.connector_address, round.dust_amount, round.version, round.swept,
round_payment_vw.id, round_payment_vw.round_id, round_payment_vw.id, round_payment_vw.round_id,
round_tx_vw.id, round_tx_vw.tx, round_tx_vw.round_id, round_tx_vw.type, round_tx_vw.position, round_tx_vw.txid, round_tx_vw.tree_level, round_tx_vw.parent_txid, round_tx_vw.is_leaf, round_tx_vw.id, round_tx_vw.tx, round_tx_vw.round_id, round_tx_vw.type, round_tx_vw.position, round_tx_vw.txid, round_tx_vw.tree_level, round_tx_vw.parent_txid, round_tx_vw.is_leaf,
payment_receiver_vw.payment_id, payment_receiver_vw.descriptor, payment_receiver_vw.amount, payment_receiver_vw.onchain_address, payment_receiver_vw.payment_id, payment_receiver_vw.pubkey, payment_receiver_vw.onchain_address, payment_receiver_vw.amount,
payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx, payment_vtxo_vw.descriptor, payment_vtxo_vw.pending payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.pubkey, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx, payment_vtxo_vw.pending
FROM round FROM round
LEFT OUTER JOIN round_payment_vw ON round.id=round_payment_vw.round_id LEFT OUTER JOIN round_payment_vw ON round.id=round_payment_vw.round_id
LEFT OUTER JOIN round_tx_vw ON round.id=round_tx_vw.round_id LEFT OUTER JOIN round_tx_vw ON round.id=round_tx_vw.round_id
@@ -260,11 +260,12 @@ func (q *Queries) SelectRoundWithRoundId(ctx context.Context, id string) ([]Sele
&i.RoundTxVw.ParentTxid, &i.RoundTxVw.ParentTxid,
&i.RoundTxVw.IsLeaf, &i.RoundTxVw.IsLeaf,
&i.PaymentReceiverVw.PaymentID, &i.PaymentReceiverVw.PaymentID,
&i.PaymentReceiverVw.Descriptor, &i.PaymentReceiverVw.Pubkey,
&i.PaymentReceiverVw.Amount,
&i.PaymentReceiverVw.OnchainAddress, &i.PaymentReceiverVw.OnchainAddress,
&i.PaymentReceiverVw.Amount,
&i.PaymentVtxoVw.Txid, &i.PaymentVtxoVw.Txid,
&i.PaymentVtxoVw.Vout, &i.PaymentVtxoVw.Vout,
&i.PaymentVtxoVw.Pubkey,
&i.PaymentVtxoVw.Amount, &i.PaymentVtxoVw.Amount,
&i.PaymentVtxoVw.PoolTx, &i.PaymentVtxoVw.PoolTx,
&i.PaymentVtxoVw.SpentBy, &i.PaymentVtxoVw.SpentBy,
@@ -274,7 +275,6 @@ func (q *Queries) SelectRoundWithRoundId(ctx context.Context, id string) ([]Sele
&i.PaymentVtxoVw.ExpireAt, &i.PaymentVtxoVw.ExpireAt,
&i.PaymentVtxoVw.PaymentID, &i.PaymentVtxoVw.PaymentID,
&i.PaymentVtxoVw.RedeemTx, &i.PaymentVtxoVw.RedeemTx,
&i.PaymentVtxoVw.Descriptor,
&i.PaymentVtxoVw.Pending, &i.PaymentVtxoVw.Pending,
); err != nil { ); err != nil {
return nil, err return nil, err
@@ -294,8 +294,8 @@ const selectRoundWithRoundTxId = `-- name: SelectRoundWithRoundTxId :many
SELECT round.id, round.starting_timestamp, round.ending_timestamp, round.ended, round.failed, round.stage_code, round.txid, round.unsigned_tx, round.connector_address, round.dust_amount, round.version, round.swept, SELECT round.id, round.starting_timestamp, round.ending_timestamp, round.ended, round.failed, round.stage_code, round.txid, round.unsigned_tx, round.connector_address, round.dust_amount, round.version, round.swept,
round_payment_vw.id, round_payment_vw.round_id, round_payment_vw.id, round_payment_vw.round_id,
round_tx_vw.id, round_tx_vw.tx, round_tx_vw.round_id, round_tx_vw.type, round_tx_vw.position, round_tx_vw.txid, round_tx_vw.tree_level, round_tx_vw.parent_txid, round_tx_vw.is_leaf, round_tx_vw.id, round_tx_vw.tx, round_tx_vw.round_id, round_tx_vw.type, round_tx_vw.position, round_tx_vw.txid, round_tx_vw.tree_level, round_tx_vw.parent_txid, round_tx_vw.is_leaf,
payment_receiver_vw.payment_id, payment_receiver_vw.descriptor, payment_receiver_vw.amount, payment_receiver_vw.onchain_address, payment_receiver_vw.payment_id, payment_receiver_vw.pubkey, payment_receiver_vw.onchain_address, payment_receiver_vw.amount,
payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx, payment_vtxo_vw.descriptor, payment_vtxo_vw.pending payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.pubkey, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx, payment_vtxo_vw.pending
FROM round FROM round
LEFT OUTER JOIN round_payment_vw ON round.id=round_payment_vw.round_id LEFT OUTER JOIN round_payment_vw ON round.id=round_payment_vw.round_id
LEFT OUTER JOIN round_tx_vw ON round.id=round_tx_vw.round_id LEFT OUTER JOIN round_tx_vw ON round.id=round_tx_vw.round_id
@@ -346,11 +346,12 @@ func (q *Queries) SelectRoundWithRoundTxId(ctx context.Context, txid string) ([]
&i.RoundTxVw.ParentTxid, &i.RoundTxVw.ParentTxid,
&i.RoundTxVw.IsLeaf, &i.RoundTxVw.IsLeaf,
&i.PaymentReceiverVw.PaymentID, &i.PaymentReceiverVw.PaymentID,
&i.PaymentReceiverVw.Descriptor, &i.PaymentReceiverVw.Pubkey,
&i.PaymentReceiverVw.Amount,
&i.PaymentReceiverVw.OnchainAddress, &i.PaymentReceiverVw.OnchainAddress,
&i.PaymentReceiverVw.Amount,
&i.PaymentVtxoVw.Txid, &i.PaymentVtxoVw.Txid,
&i.PaymentVtxoVw.Vout, &i.PaymentVtxoVw.Vout,
&i.PaymentVtxoVw.Pubkey,
&i.PaymentVtxoVw.Amount, &i.PaymentVtxoVw.Amount,
&i.PaymentVtxoVw.PoolTx, &i.PaymentVtxoVw.PoolTx,
&i.PaymentVtxoVw.SpentBy, &i.PaymentVtxoVw.SpentBy,
@@ -360,7 +361,6 @@ func (q *Queries) SelectRoundWithRoundTxId(ctx context.Context, txid string) ([]
&i.PaymentVtxoVw.ExpireAt, &i.PaymentVtxoVw.ExpireAt,
&i.PaymentVtxoVw.PaymentID, &i.PaymentVtxoVw.PaymentID,
&i.PaymentVtxoVw.RedeemTx, &i.PaymentVtxoVw.RedeemTx,
&i.PaymentVtxoVw.Descriptor,
&i.PaymentVtxoVw.Pending, &i.PaymentVtxoVw.Pending,
); err != nil { ); err != nil {
return nil, err return nil, err
@@ -380,8 +380,8 @@ const selectSweepableRounds = `-- name: SelectSweepableRounds :many
SELECT round.id, round.starting_timestamp, round.ending_timestamp, round.ended, round.failed, round.stage_code, round.txid, round.unsigned_tx, round.connector_address, round.dust_amount, round.version, round.swept, SELECT round.id, round.starting_timestamp, round.ending_timestamp, round.ended, round.failed, round.stage_code, round.txid, round.unsigned_tx, round.connector_address, round.dust_amount, round.version, round.swept,
round_payment_vw.id, round_payment_vw.round_id, round_payment_vw.id, round_payment_vw.round_id,
round_tx_vw.id, round_tx_vw.tx, round_tx_vw.round_id, round_tx_vw.type, round_tx_vw.position, round_tx_vw.txid, round_tx_vw.tree_level, round_tx_vw.parent_txid, round_tx_vw.is_leaf, round_tx_vw.id, round_tx_vw.tx, round_tx_vw.round_id, round_tx_vw.type, round_tx_vw.position, round_tx_vw.txid, round_tx_vw.tree_level, round_tx_vw.parent_txid, round_tx_vw.is_leaf,
payment_receiver_vw.payment_id, payment_receiver_vw.descriptor, payment_receiver_vw.amount, payment_receiver_vw.onchain_address, payment_receiver_vw.payment_id, payment_receiver_vw.pubkey, payment_receiver_vw.onchain_address, payment_receiver_vw.amount,
payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx, payment_vtxo_vw.descriptor, payment_vtxo_vw.pending payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.pubkey, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx, payment_vtxo_vw.pending
FROM round FROM round
LEFT OUTER JOIN round_payment_vw ON round.id=round_payment_vw.round_id LEFT OUTER JOIN round_payment_vw ON round.id=round_payment_vw.round_id
LEFT OUTER JOIN round_tx_vw ON round.id=round_tx_vw.round_id LEFT OUTER JOIN round_tx_vw ON round.id=round_tx_vw.round_id
@@ -432,11 +432,12 @@ func (q *Queries) SelectSweepableRounds(ctx context.Context) ([]SelectSweepableR
&i.RoundTxVw.ParentTxid, &i.RoundTxVw.ParentTxid,
&i.RoundTxVw.IsLeaf, &i.RoundTxVw.IsLeaf,
&i.PaymentReceiverVw.PaymentID, &i.PaymentReceiverVw.PaymentID,
&i.PaymentReceiverVw.Descriptor, &i.PaymentReceiverVw.Pubkey,
&i.PaymentReceiverVw.Amount,
&i.PaymentReceiverVw.OnchainAddress, &i.PaymentReceiverVw.OnchainAddress,
&i.PaymentReceiverVw.Amount,
&i.PaymentVtxoVw.Txid, &i.PaymentVtxoVw.Txid,
&i.PaymentVtxoVw.Vout, &i.PaymentVtxoVw.Vout,
&i.PaymentVtxoVw.Pubkey,
&i.PaymentVtxoVw.Amount, &i.PaymentVtxoVw.Amount,
&i.PaymentVtxoVw.PoolTx, &i.PaymentVtxoVw.PoolTx,
&i.PaymentVtxoVw.SpentBy, &i.PaymentVtxoVw.SpentBy,
@@ -446,7 +447,6 @@ func (q *Queries) SelectSweepableRounds(ctx context.Context) ([]SelectSweepableR
&i.PaymentVtxoVw.ExpireAt, &i.PaymentVtxoVw.ExpireAt,
&i.PaymentVtxoVw.PaymentID, &i.PaymentVtxoVw.PaymentID,
&i.PaymentVtxoVw.RedeemTx, &i.PaymentVtxoVw.RedeemTx,
&i.PaymentVtxoVw.Descriptor,
&i.PaymentVtxoVw.Pending, &i.PaymentVtxoVw.Pending,
); err != nil { ); err != nil {
return nil, err return nil, err
@@ -463,7 +463,7 @@ func (q *Queries) SelectSweepableRounds(ctx context.Context) ([]SelectSweepableR
} }
const selectSweepableVtxos = `-- name: SelectSweepableVtxos :many const selectSweepableVtxos = `-- name: SelectSweepableVtxos :many
SELECT vtxo.txid, vtxo.vout, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, vtxo.descriptor, vtxo.pending FROM vtxo SELECT vtxo.txid, vtxo.vout, vtxo.pubkey, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, vtxo.pending FROM vtxo
WHERE redeemed = false AND swept = false WHERE redeemed = false AND swept = false
` `
@@ -483,6 +483,7 @@ func (q *Queries) SelectSweepableVtxos(ctx context.Context) ([]SelectSweepableVt
if err := rows.Scan( if err := rows.Scan(
&i.Vtxo.Txid, &i.Vtxo.Txid,
&i.Vtxo.Vout, &i.Vtxo.Vout,
&i.Vtxo.Pubkey,
&i.Vtxo.Amount, &i.Vtxo.Amount,
&i.Vtxo.PoolTx, &i.Vtxo.PoolTx,
&i.Vtxo.SpentBy, &i.Vtxo.SpentBy,
@@ -492,7 +493,6 @@ func (q *Queries) SelectSweepableVtxos(ctx context.Context) ([]SelectSweepableVt
&i.Vtxo.ExpireAt, &i.Vtxo.ExpireAt,
&i.Vtxo.PaymentID, &i.Vtxo.PaymentID,
&i.Vtxo.RedeemTx, &i.Vtxo.RedeemTx,
&i.Vtxo.Descriptor,
&i.Vtxo.Pending, &i.Vtxo.Pending,
); err != nil { ); err != nil {
return nil, err return nil, err
@@ -512,8 +512,8 @@ const selectSweptRounds = `-- name: SelectSweptRounds :many
SELECT round.id, round.starting_timestamp, round.ending_timestamp, round.ended, round.failed, round.stage_code, round.txid, round.unsigned_tx, round.connector_address, round.dust_amount, round.version, round.swept, SELECT round.id, round.starting_timestamp, round.ending_timestamp, round.ended, round.failed, round.stage_code, round.txid, round.unsigned_tx, round.connector_address, round.dust_amount, round.version, round.swept,
round_payment_vw.id, round_payment_vw.round_id, round_payment_vw.id, round_payment_vw.round_id,
round_tx_vw.id, round_tx_vw.tx, round_tx_vw.round_id, round_tx_vw.type, round_tx_vw.position, round_tx_vw.txid, round_tx_vw.tree_level, round_tx_vw.parent_txid, round_tx_vw.is_leaf, round_tx_vw.id, round_tx_vw.tx, round_tx_vw.round_id, round_tx_vw.type, round_tx_vw.position, round_tx_vw.txid, round_tx_vw.tree_level, round_tx_vw.parent_txid, round_tx_vw.is_leaf,
payment_receiver_vw.payment_id, payment_receiver_vw.descriptor, payment_receiver_vw.amount, payment_receiver_vw.onchain_address, payment_receiver_vw.payment_id, payment_receiver_vw.pubkey, payment_receiver_vw.onchain_address, payment_receiver_vw.amount,
payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx, payment_vtxo_vw.descriptor, payment_vtxo_vw.pending payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.pubkey, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx, payment_vtxo_vw.pending
FROM round FROM round
LEFT OUTER JOIN round_payment_vw ON round.id=round_payment_vw.round_id LEFT OUTER JOIN round_payment_vw ON round.id=round_payment_vw.round_id
LEFT OUTER JOIN round_tx_vw ON round.id=round_tx_vw.round_id LEFT OUTER JOIN round_tx_vw ON round.id=round_tx_vw.round_id
@@ -564,11 +564,12 @@ func (q *Queries) SelectSweptRounds(ctx context.Context) ([]SelectSweptRoundsRow
&i.RoundTxVw.ParentTxid, &i.RoundTxVw.ParentTxid,
&i.RoundTxVw.IsLeaf, &i.RoundTxVw.IsLeaf,
&i.PaymentReceiverVw.PaymentID, &i.PaymentReceiverVw.PaymentID,
&i.PaymentReceiverVw.Descriptor, &i.PaymentReceiverVw.Pubkey,
&i.PaymentReceiverVw.Amount,
&i.PaymentReceiverVw.OnchainAddress, &i.PaymentReceiverVw.OnchainAddress,
&i.PaymentReceiverVw.Amount,
&i.PaymentVtxoVw.Txid, &i.PaymentVtxoVw.Txid,
&i.PaymentVtxoVw.Vout, &i.PaymentVtxoVw.Vout,
&i.PaymentVtxoVw.Pubkey,
&i.PaymentVtxoVw.Amount, &i.PaymentVtxoVw.Amount,
&i.PaymentVtxoVw.PoolTx, &i.PaymentVtxoVw.PoolTx,
&i.PaymentVtxoVw.SpentBy, &i.PaymentVtxoVw.SpentBy,
@@ -578,7 +579,6 @@ func (q *Queries) SelectSweptRounds(ctx context.Context) ([]SelectSweptRoundsRow
&i.PaymentVtxoVw.ExpireAt, &i.PaymentVtxoVw.ExpireAt,
&i.PaymentVtxoVw.PaymentID, &i.PaymentVtxoVw.PaymentID,
&i.PaymentVtxoVw.RedeemTx, &i.PaymentVtxoVw.RedeemTx,
&i.PaymentVtxoVw.Descriptor,
&i.PaymentVtxoVw.Pending, &i.PaymentVtxoVw.Pending,
); err != nil { ); err != nil {
return nil, err return nil, err
@@ -595,7 +595,7 @@ func (q *Queries) SelectSweptRounds(ctx context.Context) ([]SelectSweptRoundsRow
} }
const selectVtxoByOutpoint = `-- name: SelectVtxoByOutpoint :one const selectVtxoByOutpoint = `-- name: SelectVtxoByOutpoint :one
SELECT vtxo.txid, vtxo.vout, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, vtxo.descriptor, vtxo.pending FROM vtxo SELECT vtxo.txid, vtxo.vout, vtxo.pubkey, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, vtxo.pending FROM vtxo
WHERE txid = ? AND vout = ? WHERE txid = ? AND vout = ?
` `
@@ -614,6 +614,7 @@ func (q *Queries) SelectVtxoByOutpoint(ctx context.Context, arg SelectVtxoByOutp
err := row.Scan( err := row.Scan(
&i.Vtxo.Txid, &i.Vtxo.Txid,
&i.Vtxo.Vout, &i.Vtxo.Vout,
&i.Vtxo.Pubkey,
&i.Vtxo.Amount, &i.Vtxo.Amount,
&i.Vtxo.PoolTx, &i.Vtxo.PoolTx,
&i.Vtxo.SpentBy, &i.Vtxo.SpentBy,
@@ -623,14 +624,13 @@ func (q *Queries) SelectVtxoByOutpoint(ctx context.Context, arg SelectVtxoByOutp
&i.Vtxo.ExpireAt, &i.Vtxo.ExpireAt,
&i.Vtxo.PaymentID, &i.Vtxo.PaymentID,
&i.Vtxo.RedeemTx, &i.Vtxo.RedeemTx,
&i.Vtxo.Descriptor,
&i.Vtxo.Pending, &i.Vtxo.Pending,
) )
return i, err return i, err
} }
const selectVtxosByPoolTxid = `-- name: SelectVtxosByPoolTxid :many const selectVtxosByPoolTxid = `-- name: SelectVtxosByPoolTxid :many
SELECT vtxo.txid, vtxo.vout, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, vtxo.descriptor, vtxo.pending FROM vtxo SELECT vtxo.txid, vtxo.vout, vtxo.pubkey, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, vtxo.pending FROM vtxo
WHERE pool_tx = ? WHERE pool_tx = ?
` `
@@ -650,6 +650,7 @@ func (q *Queries) SelectVtxosByPoolTxid(ctx context.Context, poolTx string) ([]S
if err := rows.Scan( if err := rows.Scan(
&i.Vtxo.Txid, &i.Vtxo.Txid,
&i.Vtxo.Vout, &i.Vtxo.Vout,
&i.Vtxo.Pubkey,
&i.Vtxo.Amount, &i.Vtxo.Amount,
&i.Vtxo.PoolTx, &i.Vtxo.PoolTx,
&i.Vtxo.SpentBy, &i.Vtxo.SpentBy,
@@ -659,7 +660,6 @@ func (q *Queries) SelectVtxosByPoolTxid(ctx context.Context, poolTx string) ([]S
&i.Vtxo.ExpireAt, &i.Vtxo.ExpireAt,
&i.Vtxo.PaymentID, &i.Vtxo.PaymentID,
&i.Vtxo.RedeemTx, &i.Vtxo.RedeemTx,
&i.Vtxo.Descriptor,
&i.Vtxo.Pending, &i.Vtxo.Pending,
); err != nil { ); err != nil {
return nil, err return nil, err
@@ -721,26 +721,26 @@ func (q *Queries) UpsertPayment(ctx context.Context, arg UpsertPaymentParams) er
} }
const upsertReceiver = `-- name: UpsertReceiver :exec const upsertReceiver = `-- name: UpsertReceiver :exec
INSERT INTO receiver (payment_id, descriptor, amount, onchain_address) VALUES (?, ?, ?, ?) INSERT INTO receiver (payment_id, pubkey, onchain_address, amount) VALUES (?, ?, ?, ?)
ON CONFLICT(payment_id, descriptor) DO UPDATE SET ON CONFLICT(payment_id, pubkey, onchain_address) DO UPDATE SET
amount = EXCLUDED.amount, amount = EXCLUDED.amount,
onchain_address = EXCLUDED.onchain_address, pubkey = EXCLUDED.pubkey,
descriptor = EXCLUDED.descriptor onchain_address = EXCLUDED.onchain_address
` `
type UpsertReceiverParams struct { type UpsertReceiverParams struct {
PaymentID string PaymentID string
Descriptor string Pubkey sql.NullString
OnchainAddress sql.NullString
Amount int64 Amount int64
OnchainAddress string
} }
func (q *Queries) UpsertReceiver(ctx context.Context, arg UpsertReceiverParams) error { func (q *Queries) UpsertReceiver(ctx context.Context, arg UpsertReceiverParams) error {
_, err := q.db.ExecContext(ctx, upsertReceiver, _, err := q.db.ExecContext(ctx, upsertReceiver,
arg.PaymentID, arg.PaymentID,
arg.Descriptor, arg.Pubkey,
arg.Amount,
arg.OnchainAddress, arg.OnchainAddress,
arg.Amount,
) )
return err return err
} }
@@ -847,9 +847,9 @@ func (q *Queries) UpsertTransaction(ctx context.Context, arg UpsertTransactionPa
} }
const upsertVtxo = `-- name: UpsertVtxo :exec const upsertVtxo = `-- name: UpsertVtxo :exec
INSERT INTO vtxo (txid, vout, descriptor, amount, pool_tx, spent_by, spent, redeemed, swept, expire_at, redeem_tx, pending) INSERT INTO vtxo (txid, vout, pubkey, amount, pool_tx, spent_by, spent, redeemed, swept, expire_at, redeem_tx, pending)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(txid, vout) DO UPDATE SET VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(txid, vout) DO UPDATE SET
descriptor = EXCLUDED.descriptor, pubkey = EXCLUDED.pubkey,
amount = EXCLUDED.amount, amount = EXCLUDED.amount,
pool_tx = EXCLUDED.pool_tx, pool_tx = EXCLUDED.pool_tx,
spent_by = EXCLUDED.spent_by, spent_by = EXCLUDED.spent_by,
@@ -862,25 +862,25 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(txid, vout) DO UPDATE SE
` `
type UpsertVtxoParams struct { type UpsertVtxoParams struct {
Txid string Txid string
Vout int64 Vout int64
Descriptor sql.NullString Pubkey string
Amount int64 Amount int64
PoolTx string PoolTx string
SpentBy string SpentBy string
Spent bool Spent bool
Redeemed bool Redeemed bool
Swept bool Swept bool
ExpireAt int64 ExpireAt int64
RedeemTx sql.NullString RedeemTx sql.NullString
Pending bool Pending bool
} }
func (q *Queries) UpsertVtxo(ctx context.Context, arg UpsertVtxoParams) error { func (q *Queries) UpsertVtxo(ctx context.Context, arg UpsertVtxoParams) error {
_, err := q.db.ExecContext(ctx, upsertVtxo, _, err := q.db.ExecContext(ctx, upsertVtxo,
arg.Txid, arg.Txid,
arg.Vout, arg.Vout,
arg.Descriptor, arg.Pubkey,
arg.Amount, arg.Amount,
arg.PoolTx, arg.PoolTx,
arg.SpentBy, arg.SpentBy,

View File

@@ -44,11 +44,11 @@ INSERT INTO payment (id, round_id) VALUES (?, ?)
ON CONFLICT(id) DO UPDATE SET round_id = EXCLUDED.round_id; ON CONFLICT(id) DO UPDATE SET round_id = EXCLUDED.round_id;
-- name: UpsertReceiver :exec -- name: UpsertReceiver :exec
INSERT INTO receiver (payment_id, descriptor, amount, onchain_address) VALUES (?, ?, ?, ?) INSERT INTO receiver (payment_id, pubkey, onchain_address, amount) VALUES (?, ?, ?, ?)
ON CONFLICT(payment_id, descriptor) DO UPDATE SET ON CONFLICT(payment_id, pubkey, onchain_address) DO UPDATE SET
amount = EXCLUDED.amount, amount = EXCLUDED.amount,
onchain_address = EXCLUDED.onchain_address, pubkey = EXCLUDED.pubkey,
descriptor = EXCLUDED.descriptor; onchain_address = EXCLUDED.onchain_address;
-- name: UpdateVtxoPaymentId :exec -- name: UpdateVtxoPaymentId :exec
UPDATE vtxo SET payment_id = ? WHERE txid = ? AND vout = ?; UPDATE vtxo SET payment_id = ? WHERE txid = ? AND vout = ?;
@@ -112,9 +112,9 @@ SELECT id FROM round WHERE starting_timestamp > ? AND starting_timestamp < ?;
SELECT id FROM round; SELECT id FROM round;
-- name: UpsertVtxo :exec -- name: UpsertVtxo :exec
INSERT INTO vtxo (txid, vout, descriptor, amount, pool_tx, spent_by, spent, redeemed, swept, expire_at, redeem_tx, pending) INSERT INTO vtxo (txid, vout, pubkey, amount, pool_tx, spent_by, spent, redeemed, swept, expire_at, redeem_tx, pending)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(txid, vout) DO UPDATE SET VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(txid, vout) DO UPDATE SET
descriptor = EXCLUDED.descriptor, pubkey = EXCLUDED.pubkey,
amount = EXCLUDED.amount, amount = EXCLUDED.amount,
pool_tx = EXCLUDED.pool_tx, pool_tx = EXCLUDED.pool_tx,
spent_by = EXCLUDED.spent_by, spent_by = EXCLUDED.spent_by,
@@ -135,7 +135,7 @@ WHERE redeemed = false;
-- name: SelectNotRedeemedVtxosWithPubkey :many -- name: SelectNotRedeemedVtxosWithPubkey :many
SELECT sqlc.embed(vtxo) FROM vtxo SELECT sqlc.embed(vtxo) FROM vtxo
WHERE redeemed = false AND INSTR(descriptor, ?) > 0; WHERE redeemed = false AND pubkey = ?;
-- name: SelectVtxoByOutpoint :one -- name: SelectVtxoByOutpoint :one
SELECT sqlc.embed(vtxo) FROM vtxo SELECT sqlc.embed(vtxo) FROM vtxo

View File

@@ -37,20 +37,21 @@ func (v *vxtoRepository) AddVtxos(ctx context.Context, vtxos []domain.Vtxo) erro
txBody := func(querierWithTx *queries.Queries) error { txBody := func(querierWithTx *queries.Queries) error {
for i := range vtxos { for i := range vtxos {
vtxo := vtxos[i] vtxo := vtxos[i]
if err := querierWithTx.UpsertVtxo( if err := querierWithTx.UpsertVtxo(
ctx, queries.UpsertVtxoParams{ ctx, queries.UpsertVtxoParams{
Txid: vtxo.Txid, Txid: vtxo.Txid,
Vout: int64(vtxo.VOut), Vout: int64(vtxo.VOut),
Descriptor: sql.NullString{String: vtxo.Descriptor, Valid: true}, Pubkey: vtxo.Pubkey,
Amount: int64(vtxo.Amount), Amount: int64(vtxo.Amount),
PoolTx: vtxo.RoundTxid, PoolTx: vtxo.RoundTxid,
SpentBy: vtxo.SpentBy, SpentBy: vtxo.SpentBy,
Spent: vtxo.Spent, Spent: vtxo.Spent,
Redeemed: vtxo.Redeemed, Redeemed: vtxo.Redeemed,
Swept: vtxo.Swept, Swept: vtxo.Swept,
ExpireAt: vtxo.ExpireAt, ExpireAt: vtxo.ExpireAt,
RedeemTx: sql.NullString{String: vtxo.RedeemTx, Valid: true}, RedeemTx: sql.NullString{String: vtxo.RedeemTx, Valid: true},
Pending: vtxo.Pending, Pending: vtxo.Pending,
}, },
); err != nil { ); err != nil {
return err return err
@@ -81,10 +82,6 @@ func (v *vxtoRepository) GetAllVtxos(ctx context.Context, pubkey string) ([]doma
var rows []queries.Vtxo var rows []queries.Vtxo
if withPubkey { if withPubkey {
if len(pubkey) == 66 {
pubkey = pubkey[2:]
}
res, err := v.querier.SelectNotRedeemedVtxosWithPubkey(ctx, pubkey) res, err := v.querier.SelectNotRedeemedVtxosWithPubkey(ctx, pubkey)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@@ -253,10 +250,8 @@ func rowToVtxo(row queries.Vtxo) domain.Vtxo {
Txid: row.Txid, Txid: row.Txid,
VOut: uint32(row.Vout), VOut: uint32(row.Vout),
}, },
Receiver: domain.Receiver{ Amount: uint64(row.Amount),
Descriptor: row.Descriptor.String, Pubkey: row.Pubkey,
Amount: uint64(row.Amount),
},
RoundTxid: row.PoolTx, RoundTxid: row.PoolTx,
SpentBy: row.SpentBy, SpentBy: row.SpentBy,
Spent: row.Spent, Spent: row.Spent,

View File

@@ -84,6 +84,7 @@ func (b *txBuilder) BuildSweepTx(inputs []ports.SweepInput) (signedSweepTx strin
func (b *txBuilder) BuildForfeitTxs( func (b *txBuilder) BuildForfeitTxs(
poolTx string, poolTx string,
payments []domain.Payment, payments []domain.Payment,
descriptors map[domain.VtxoKey]string,
minRelayFeeRate chainfee.SatPerKVByte, minRelayFeeRate chainfee.SatPerKVByte,
) (connectors []string, forfeitTxs []string, err error) { ) (connectors []string, forfeitTxs []string, err error) {
connectorAddress, err := b.getConnectorAddress(poolTx) connectorAddress, err := b.getConnectorAddress(poolTx)
@@ -106,7 +107,7 @@ func (b *txBuilder) BuildForfeitTxs(
return nil, nil, err return nil, nil, err
} }
forfeitTxs, err = b.createForfeitTxs(payments, connectorTxs, connectorAmount, minRelayFeeRate) forfeitTxs, err = b.createForfeitTxs(payments, descriptors, connectorTxs, connectorAmount, minRelayFeeRate)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -147,13 +148,13 @@ func (b *txBuilder) BuildRoundTx(
return "", nil, "", err return "", nil, "", err
} }
receivers, err := getOffchainReceivers(payments) vtxosLeaves, err := getOutputVtxosLeaves(payments)
if err != nil { if err != nil {
return "", nil, "", err return "", nil, "", err
} }
treeFactoryFn, sharedOutputScript, sharedOutputAmount, err = tree.CraftCongestionTree( treeFactoryFn, sharedOutputScript, sharedOutputAmount, err = tree.CraftCongestionTree(
b.onchainNetwork().AssetID, aspPubkey, receivers, feeSatsPerNode, b.roundLifetime, b.onchainNetwork().AssetID, aspPubkey, vtxosLeaves, feeSatsPerNode, b.roundLifetime,
) )
if err != nil { if err != nil {
return "", nil, "", err return "", nil, "", err
@@ -362,7 +363,10 @@ func (b *txBuilder) FindLeaves(
} }
func (b *txBuilder) BuildAsyncPaymentTransactions( func (b *txBuilder) BuildAsyncPaymentTransactions(
_ []domain.Vtxo, _ *secp256k1.PublicKey, _ []domain.Receiver, _ []domain.Vtxo,
_ map[domain.VtxoKey]string,
_ map[domain.VtxoKey]chainhash.Hash,
_ []domain.Receiver,
) (string, error) { ) (string, error) {
return "", fmt.Errorf("not implemented") return "", fmt.Errorf("not implemented")
} }
@@ -396,7 +400,6 @@ func (b *txBuilder) createPoolTx(
return nil, err return nil, err
} }
receivers := getOnchainReceivers(payments)
nbOfInputs := countSpentVtxos(payments) nbOfInputs := countSpentVtxos(payments)
connectorsAmount := (dustAmount + connectorMinRelayFee) * nbOfInputs connectorsAmount := (dustAmount + connectorMinRelayFee) * nbOfInputs
if nbOfInputs > 1 { if nbOfInputs > 1 {
@@ -424,21 +427,17 @@ func (b *txBuilder) createPoolTx(
}) })
} }
for _, receiver := range receivers { onchainOutputs, err := getOnchainOutputs(payments, b.onchainNetwork())
targetAmount += receiver.Amount if err != nil {
return nil, err
receiverScript, err := address.ToOutputScript(receiver.OnchainAddress)
if err != nil {
return nil, err
}
outputs = append(outputs, psetv2.OutputArgs{
Asset: b.onchainNetwork().AssetID,
Amount: receiver.Amount,
Script: receiverScript,
})
} }
for _, out := range onchainOutputs {
targetAmount += out.Amount
}
outputs = append(outputs, onchainOutputs...)
for _, in := range boardingInputs { for _, in := range boardingInputs {
targetAmount -= in.Amount targetAmount -= in.Amount
} }
@@ -786,6 +785,7 @@ func (b *txBuilder) createConnectors(
func (b *txBuilder) createForfeitTxs( func (b *txBuilder) createForfeitTxs(
payments []domain.Payment, payments []domain.Payment,
descriptors map[domain.VtxoKey]string,
connectors []*psetv2.Pset, connectors []*psetv2.Pset,
connectorAmount uint64, connectorAmount uint64,
minRelayFeeRate chainfee.SatPerKVByte, minRelayFeeRate chainfee.SatPerKVByte,
@@ -803,7 +803,12 @@ func (b *txBuilder) createForfeitTxs(
forfeitTxs := make([]string, 0) forfeitTxs := make([]string, 0)
for _, payment := range payments { for _, payment := range payments {
for _, vtxo := range payment.Inputs { for _, vtxo := range payment.Inputs {
offchainScript, err := tree.ParseVtxoScript(vtxo.Descriptor) desc, ok := descriptors[vtxo.VtxoKey]
if !ok {
return nil, fmt.Errorf("descriptor not found for vtxo %s:%d", vtxo.VtxoKey.Txid, vtxo.VtxoKey.VOut)
}
offchainScript, err := tree.ParseVtxoScript(desc)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -113,7 +113,7 @@ func TestBuildForfeitTxs(t *testing.T) {
t.Run("valid", func(t *testing.T) { t.Run("valid", func(t *testing.T) {
for _, f := range fixtures.Valid { for _, f := range fixtures.Valid {
connectors, forfeitTxs, err := builder.BuildForfeitTxs( connectors, forfeitTxs, err := builder.BuildForfeitTxs(
f.PoolTx, f.Payments, minRelayFeeRate, f.PoolTx, f.Payments, f.Descriptors, minRelayFeeRate,
) )
require.NoError(t, err) require.NoError(t, err)
require.Len(t, connectors, f.ExpectedNumOfConnectors) require.Len(t, connectors, f.ExpectedNumOfConnectors)
@@ -151,7 +151,7 @@ func TestBuildForfeitTxs(t *testing.T) {
t.Run("invalid", func(t *testing.T) { t.Run("invalid", func(t *testing.T) {
for _, f := range fixtures.Invalid { for _, f := range fixtures.Invalid {
connectors, forfeitTxs, err := builder.BuildForfeitTxs( connectors, forfeitTxs, err := builder.BuildForfeitTxs(
f.PoolTx, f.Payments, minRelayFeeRate, f.PoolTx, f.Payments, f.Descriptors, minRelayFeeRate,
) )
require.EqualError(t, err, f.ExpectedErr) require.EqualError(t, err, f.ExpectedErr)
require.Empty(t, connectors) require.Empty(t, connectors)
@@ -215,6 +215,7 @@ func parsePoolTxFixtures() (*poolTxFixtures, error) {
type forfeitTxsFixtures struct { type forfeitTxsFixtures struct {
Valid []struct { Valid []struct {
Payments []domain.Payment Payments []domain.Payment
Descriptors map[domain.VtxoKey]string
ExpectedNumOfConnectors int ExpectedNumOfConnectors int
ExpectedNumOfForfeitTxs int ExpectedNumOfForfeitTxs int
PoolTx string PoolTx string
@@ -222,6 +223,7 @@ type forfeitTxsFixtures struct {
} }
Invalid []struct { Invalid []struct {
Payments []domain.Payment Payments []domain.Payment
Descriptors map[domain.VtxoKey]string
ExpectedErr string ExpectedErr string
PoolTx string PoolTx string
} }
@@ -244,6 +246,42 @@ func parseForfeitTxsFixtures() (*forfeitTxsFixtures, error) {
return nil, err return nil, err
} }
valid := vv["valid"].([]interface{})
for i, v := range valid {
val := v.(map[string]interface{})
payments := val["payments"].([]interface{})
descriptors := make(map[domain.VtxoKey]string)
for _, p := range payments {
inputs := p.(map[string]interface{})["inputs"].([]interface{})
for _, in := range inputs {
inMap := in.(map[string]interface{})
descriptors[domain.VtxoKey{
Txid: inMap["txid"].(string),
VOut: uint32(inMap["vout"].(float64)),
}] = inMap["descriptor"].(string)
}
}
fixtures.Valid[i].Descriptors = descriptors
}
invalid := vv["invalid"].([]interface{})
for i, v := range invalid {
val := v.(map[string]interface{})
payments := val["payments"].([]interface{})
descriptors := make(map[domain.VtxoKey]string)
for _, p := range payments {
inputs := p.(map[string]interface{})["inputs"].([]interface{})
for _, in := range inputs {
inMap := in.(map[string]interface{})
descriptors[domain.VtxoKey{
Txid: inMap["txid"].(string),
VOut: uint32(inMap["vout"].(float64)),
}] = inMap["descriptor"].(string)
}
}
fixtures.Invalid[i].Descriptors = descriptors
}
return &fixtures, nil return &fixtures, nil
} }

View File

@@ -9,14 +9,14 @@
{ {
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", "txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0, "vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 1100 "amount": 1100
} }
], ],
"receivers": [ "receivers": [
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 1100 "amount": 1100
} }
] ]
@@ -33,18 +33,18 @@
{ {
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", "txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0, "vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 1100 "amount": 1100
} }
], ],
"receivers": [ "receivers": [
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 600 "amount": 600
}, },
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 500 "amount": 500
} }
] ]
@@ -61,18 +61,18 @@
{ {
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", "txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0, "vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 1100 "amount": 1100
} }
], ],
"receivers": [ "receivers": [
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 600 "amount": 600
}, },
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 500 "amount": 500
} }
] ]
@@ -83,18 +83,18 @@
{ {
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", "txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0, "vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 1100 "amount": 1100
} }
], ],
"receivers": [ "receivers": [
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 600 "amount": 600
}, },
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 500 "amount": 500
} }
] ]
@@ -105,18 +105,18 @@
{ {
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", "txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0, "vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 1100 "amount": 1100
} }
], ],
"receivers": [ "receivers": [
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 600 "amount": 600
}, },
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 500 "amount": 500
} }
] ]
@@ -133,58 +133,58 @@
{ {
"txid": "755c820771284d85ea4bbcc246565b4eddadc44237a7e57a0f9cb78a840d1d41", "txid": "755c820771284d85ea4bbcc246565b4eddadc44237a7e57a0f9cb78a840d1d41",
"vout": 0, "vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 1000 "amount": 1000
}, },
{ {
"txid": "66a0df86fcdeb84b8877adfe0b2c556dba30305d72ddbd4c49355f6930355357", "txid": "66a0df86fcdeb84b8877adfe0b2c556dba30305d72ddbd4c49355f6930355357",
"vout": 0, "vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 1000 "amount": 1000
}, },
{ {
"txid": "9913159bc7aa493ca53cbb9cbc88f97ba01137c814009dc7ef520c3fafc67909", "txid": "9913159bc7aa493ca53cbb9cbc88f97ba01137c814009dc7ef520c3fafc67909",
"vout": 1, "vout": 1,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 500 "amount": 500
}, },
{ {
"txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60", "txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60",
"vout": 0, "vout": 0,
"pubkey":"0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 1000 "amount": 1000
}, },
{ {
"txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60", "txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60",
"vout": 1, "vout": 1,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 1000 "amount": 1000
} }
], ],
"receivers": [ "receivers": [
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 1000 "amount": 1000
}, },
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 1000 "amount": 1000
}, },
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 1000 "amount": 1000
}, },
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 1000 "amount": 1000
}, },
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 500 "amount": 500
} }
] ]
@@ -206,25 +206,25 @@
{ {
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", "txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0, "vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 600 "amount": 600
}, },
{ {
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", "txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 1, "vout": 1,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 500 "amount": 500
} }
], ],
"receivers": [ "receivers": [
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 600 "amount": 600
}, },
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 500 "amount": 500
} }
] ]

View File

@@ -45,34 +45,38 @@ func getPsetId(pset *psetv2.Pset) (string, error) {
return utx.TxHash().String(), nil return utx.TxHash().String(), nil
} }
func getOnchainReceivers( func getOnchainOutputs(
payments []domain.Payment, payments []domain.Payment, net *network.Network,
) []domain.Receiver { ) ([]psetv2.OutputArgs, error) {
receivers := make([]domain.Receiver, 0) outputs := make([]psetv2.OutputArgs, 0)
for _, payment := range payments { for _, payment := range payments {
for _, receiver := range payment.Receivers { for _, receiver := range payment.Receivers {
if receiver.IsOnchain() { if receiver.IsOnchain() {
receivers = append(receivers, receiver) receiverScript, err := address.ToOutputScript(receiver.OnchainAddress)
}
}
}
return receivers
}
func getOffchainReceivers(
payments []domain.Payment,
) ([]tree.Receiver, error) {
receivers := make([]tree.Receiver, 0)
for _, payment := range payments {
for _, receiver := range payment.Receivers {
if !receiver.IsOnchain() {
vtxoScript, err := tree.ParseVtxoScript(receiver.Descriptor)
if err != nil { if err != nil {
return nil, err return nil, err
} }
receivers = append(receivers, tree.Receiver{ outputs = append(outputs, psetv2.OutputArgs{
Script: vtxoScript, Script: receiverScript,
Amount: receiver.Amount,
Asset: net.AssetID,
})
}
}
}
return outputs, nil
}
func getOutputVtxosLeaves(
payments []domain.Payment,
) ([]tree.VtxoLeaf, error) {
receivers := make([]tree.VtxoLeaf, 0)
for _, payment := range payments {
for _, receiver := range payment.Receivers {
if !receiver.IsOnchain() {
receivers = append(receivers, tree.VtxoLeaf{
Pubkey: receiver.Pubkey,
Amount: receiver.Amount, Amount: receiver.Amount,
}) })
} }

View File

@@ -228,7 +228,10 @@ func (b *txBuilder) BuildSweepTx(inputs []ports.SweepInput) (signedSweepTx strin
} }
func (b *txBuilder) BuildForfeitTxs( func (b *txBuilder) BuildForfeitTxs(
poolTx string, payments []domain.Payment, minRelayFeeRate chainfee.SatPerKVByte, poolTx string,
payments []domain.Payment,
descriptors map[domain.VtxoKey]string,
minRelayFeeRate chainfee.SatPerKVByte,
) (connectors []string, forfeitTxs []string, err error) { ) (connectors []string, forfeitTxs []string, err error) {
connectorPkScript, err := b.getConnectorPkScript(poolTx) connectorPkScript, err := b.getConnectorPkScript(poolTx)
if err != nil { if err != nil {
@@ -245,7 +248,7 @@ func (b *txBuilder) BuildForfeitTxs(
return nil, nil, err return nil, nil, err
} }
forfeitTxs, err = b.createForfeitTxs(payments, connectorTxs, minRelayFeeRate) forfeitTxs, err = b.createForfeitTxs(payments, descriptors, connectorTxs, minRelayFeeRate)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -271,7 +274,7 @@ func (b *txBuilder) BuildRoundTx(
return "", nil, "", fmt.Errorf("missing cosigners") return "", nil, "", fmt.Errorf("missing cosigners")
} }
receivers, err := getOffchainReceivers(payments) receivers, err := getOutputVtxosLeaves(payments)
if err != nil { if err != nil {
return "", nil, "", err return "", nil, "", err
} }
@@ -399,7 +402,10 @@ func (b *txBuilder) FindLeaves(congestionTree tree.CongestionTree, fromtxid stri
} }
func (b *txBuilder) BuildAsyncPaymentTransactions( func (b *txBuilder) BuildAsyncPaymentTransactions(
vtxos []domain.Vtxo, aspPubKey *secp256k1.PublicKey, receivers []domain.Receiver, vtxos []domain.Vtxo,
descriptors map[domain.VtxoKey]string,
forfeitsLeaves map[domain.VtxoKey]chainhash.Hash,
receivers []domain.Receiver,
) (string, error) { ) (string, error) {
if len(vtxos) <= 0 { if len(vtxos) <= 0 {
return "", fmt.Errorf("missing vtxos") return "", fmt.Errorf("missing vtxos")
@@ -412,6 +418,16 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
redeemTxWeightEstimator := &input.TxWeightEstimator{} redeemTxWeightEstimator := &input.TxWeightEstimator{}
for index, vtxo := range vtxos { for index, vtxo := range vtxos {
desc, ok := descriptors[vtxo.VtxoKey]
if !ok {
return "", fmt.Errorf("missing descriptor for vtxo %s", vtxo.VtxoKey)
}
forfeitLeafHash, ok := forfeitsLeaves[vtxo.VtxoKey]
if !ok {
return "", fmt.Errorf("missing forfeit leaf hash for vtxo %s", vtxo.VtxoKey)
}
if vtxo.Spent || vtxo.Redeemed || vtxo.Swept { if vtxo.Spent || vtxo.Redeemed || vtxo.Swept {
return "", fmt.Errorf("all vtxos must be unspent") return "", fmt.Errorf("all vtxos must be unspent")
} }
@@ -426,7 +442,7 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
Index: vtxo.VOut, Index: vtxo.VOut,
} }
vtxoScript, err := bitcointree.ParseVtxoScript(vtxo.Descriptor) vtxoScript, err := bitcointree.ParseVtxoScript(desc)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -446,41 +462,27 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
PkScript: vtxoOutputScript, PkScript: vtxoOutputScript,
} }
if defaultVtxoScript, ok := vtxoScript.(*bitcointree.DefaultVtxoScript); ok { leafProof, err := vtxoTree.GetTaprootMerkleProof(forfeitLeafHash)
forfeitLeaf := bitcointree.MultisigClosure{ if err != nil {
Pubkey: defaultVtxoScript.Owner, return "", err
AspPubkey: defaultVtxoScript.Asp,
}
tapLeaf, err := forfeitLeaf.Leaf()
if err != nil {
return "", err
}
leafProof, err := vtxoTree.GetTaprootMerkleProof(tapLeaf.TapHash())
if err != nil {
return "", err
}
tapscripts[index] = &psbt.TaprootTapLeafScript{
ControlBlock: leafProof.ControlBlock,
Script: leafProof.Script,
LeafVersion: txscript.BaseLeafVersion,
}
ctrlBlock, err := txscript.ParseControlBlock(leafProof.ControlBlock)
if err != nil {
return "", err
}
redeemTxWeightEstimator.AddTapscriptInput(64*2, &waddrmgr.Tapscript{
RevealedScript: leafProof.Script,
ControlBlock: ctrlBlock,
})
} else {
return "", fmt.Errorf("vtxo %s:%d script is not default script, can't be async spent", vtxo.Txid, vtxo.VOut)
} }
tapscripts[index] = &psbt.TaprootTapLeafScript{
ControlBlock: leafProof.ControlBlock,
Script: leafProof.Script,
LeafVersion: txscript.BaseLeafVersion,
}
ctrlBlock, err := txscript.ParseControlBlock(leafProof.ControlBlock)
if err != nil {
return "", err
}
redeemTxWeightEstimator.AddTapscriptInput(64*2+40, &waddrmgr.Tapscript{
RevealedScript: leafProof.Script,
ControlBlock: ctrlBlock,
})
ins = append(ins, vtxoOutpoint) ins = append(ins, vtxoOutpoint)
} }
@@ -498,17 +500,21 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
} }
for i, receiver := range receivers { for i, receiver := range receivers {
offchainScript, err := bitcointree.ParseVtxoScript(receiver.Descriptor) if receiver.IsOnchain() {
return "", fmt.Errorf("receiver %d is onchain", i)
}
pubkeyBytes, err := hex.DecodeString(receiver.Pubkey)
if err != nil { if err != nil {
return "", err return "", err
} }
receiverVtxoTaprootKey, _, err := offchainScript.TapTree() pubkey, err := schnorr.ParsePubKey(pubkeyBytes)
if err != nil { if err != nil {
return "", err return "", err
} }
newVtxoScript, err := common.P2TRScript(receiverVtxoTaprootKey) newVtxoScript, err := common.P2TRScript(pubkey)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -588,7 +594,6 @@ func (b *txBuilder) createRoundTx(
connectorAmount := dustLimit connectorAmount := dustLimit
receivers := getOnchainReceivers(payments)
nbOfInputs := countSpentVtxos(payments) nbOfInputs := countSpentVtxos(payments)
connectorsAmount := (connectorAmount + connectorMinRelayFee) * nbOfInputs connectorsAmount := (connectorAmount + connectorMinRelayFee) * nbOfInputs
if nbOfInputs > 1 { if nbOfInputs > 1 {
@@ -614,25 +619,17 @@ func (b *txBuilder) createRoundTx(
}) })
} }
for _, receiver := range receivers { onchainOutputs, err := getOnchainOutputs(payments, b.onchainNetwork())
targetAmount += receiver.Amount if err != nil {
return nil, err
receiverAddr, err := btcutil.DecodeAddress(receiver.OnchainAddress, b.onchainNetwork())
if err != nil {
return nil, err
}
receiverScript, err := txscript.PayToAddrScript(receiverAddr)
if err != nil {
return nil, err
}
outputs = append(outputs, &wire.TxOut{
Value: int64(receiver.Amount),
PkScript: receiverScript,
})
} }
for _, output := range onchainOutputs {
targetAmount += uint64(output.Value)
}
outputs = append(outputs, onchainOutputs...)
for _, input := range boardingInputs { for _, input := range boardingInputs {
targetAmount -= input.Amount targetAmount -= input.Amount
} }
@@ -1016,6 +1013,7 @@ func (b *txBuilder) minRelayFeeTreeTx() (uint64, error) {
func (b *txBuilder) createForfeitTxs( func (b *txBuilder) createForfeitTxs(
payments []domain.Payment, payments []domain.Payment,
descriptors map[domain.VtxoKey]string,
connectors []*psbt.Packet, connectors []*psbt.Packet,
minRelayFeeRate chainfee.SatPerKVByte, minRelayFeeRate chainfee.SatPerKVByte,
) ([]string, error) { ) ([]string, error) {
@@ -1042,7 +1040,12 @@ func (b *txBuilder) createForfeitTxs(
forfeitTxs := make([]string, 0) forfeitTxs := make([]string, 0)
for _, payment := range payments { for _, payment := range payments {
for _, vtxo := range payment.Inputs { for _, vtxo := range payment.Inputs {
offchainscript, err := bitcointree.ParseVtxoScript(vtxo.Descriptor) desc, ok := descriptors[vtxo.VtxoKey]
if !ok {
return nil, err
}
offchainscript, err := bitcointree.ParseVtxoScript(desc)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -123,7 +123,7 @@ func TestBuildForfeitTxs(t *testing.T) {
t.Run("valid", func(t *testing.T) { t.Run("valid", func(t *testing.T) {
for _, f := range fixtures.Valid { for _, f := range fixtures.Valid {
connectors, forfeitTxs, err := builder.BuildForfeitTxs( connectors, forfeitTxs, err := builder.BuildForfeitTxs(
f.PoolTx, f.Payments, minRelayFeeRate, f.PoolTx, f.Payments, f.Descriptors, minRelayFeeRate,
) )
require.NoError(t, err) require.NoError(t, err)
require.Len(t, connectors, f.ExpectedNumOfConnectors) require.Len(t, connectors, f.ExpectedNumOfConnectors)
@@ -161,7 +161,7 @@ func TestBuildForfeitTxs(t *testing.T) {
t.Run("invalid", func(t *testing.T) { t.Run("invalid", func(t *testing.T) {
for _, f := range fixtures.Invalid { for _, f := range fixtures.Invalid {
connectors, forfeitTxs, err := builder.BuildForfeitTxs( connectors, forfeitTxs, err := builder.BuildForfeitTxs(
f.PoolTx, f.Payments, minRelayFeeRate, f.PoolTx, f.Payments, f.Descriptors, minRelayFeeRate,
) )
require.EqualError(t, err, f.ExpectedErr) require.EqualError(t, err, f.ExpectedErr)
require.Empty(t, connectors) require.Empty(t, connectors)
@@ -225,6 +225,7 @@ func parsePoolTxFixtures() (*poolTxFixtures, error) {
type forfeitTxsFixtures struct { type forfeitTxsFixtures struct {
Valid []struct { Valid []struct {
Payments []domain.Payment Payments []domain.Payment
Descriptors map[domain.VtxoKey]string
ExpectedNumOfConnectors int ExpectedNumOfConnectors int
ExpectedNumOfForfeitTxs int ExpectedNumOfForfeitTxs int
PoolTx string PoolTx string
@@ -232,6 +233,7 @@ type forfeitTxsFixtures struct {
} }
Invalid []struct { Invalid []struct {
Payments []domain.Payment Payments []domain.Payment
Descriptors map[domain.VtxoKey]string
ExpectedErr string ExpectedErr string
PoolTx string PoolTx string
} }
@@ -254,5 +256,41 @@ func parseForfeitTxsFixtures() (*forfeitTxsFixtures, error) {
return nil, err return nil, err
} }
valid := vv["valid"].([]interface{})
for i, v := range valid {
val := v.(map[string]interface{})
payments := val["payments"].([]interface{})
descriptors := make(map[domain.VtxoKey]string)
for _, p := range payments {
inputs := p.(map[string]interface{})["inputs"].([]interface{})
for _, in := range inputs {
inMap := in.(map[string]interface{})
descriptors[domain.VtxoKey{
Txid: inMap["txid"].(string),
VOut: uint32(inMap["vout"].(float64)),
}] = inMap["descriptor"].(string)
}
}
fixtures.Valid[i].Descriptors = descriptors
}
invalid := vv["invalid"].([]interface{})
for i, v := range invalid {
val := v.(map[string]interface{})
payments := val["payments"].([]interface{})
descriptors := make(map[domain.VtxoKey]string)
for _, p := range payments {
inputs := p.(map[string]interface{})["inputs"].([]interface{})
for _, in := range inputs {
inMap := in.(map[string]interface{})
descriptors[domain.VtxoKey{
Txid: inMap["txid"].(string),
VOut: uint32(inMap["vout"].(float64)),
}] = inMap["descriptor"].(string)
}
}
fixtures.Invalid[i].Descriptors = descriptors
}
return &fixtures, nil return &fixtures, nil
} }

View File

@@ -9,13 +9,14 @@
{ {
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", "txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0, "vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1100 "amount": 1100
} }
], ],
"receivers": [ "receivers": [
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 1100 "amount": 1100
} }
] ]
@@ -32,17 +33,18 @@
{ {
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", "txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0, "vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1100 "amount": 1100
} }
], ],
"receivers": [ "receivers": [
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 600 "amount": 600
}, },
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 500 "amount": 500
} }
] ]
@@ -59,17 +61,18 @@
{ {
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", "txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0, "vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1100 "amount": 1100
} }
], ],
"receivers": [ "receivers": [
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 600 "amount": 600
}, },
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 500 "amount": 500
} }
] ]
@@ -80,17 +83,18 @@
{ {
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", "txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0, "vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1100 "amount": 1100
} }
], ],
"receivers": [ "receivers": [
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 600 "amount": 600
}, },
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 500 "amount": 500
} }
] ]
@@ -101,17 +105,18 @@
{ {
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", "txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0, "vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1100 "amount": 1100
} }
], ],
"receivers": [ "receivers": [
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 600 "amount": 600
}, },
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 500 "amount": 500
} }
] ]
@@ -128,54 +133,58 @@
{ {
"txid": "755c820771284d85ea4bbcc246565b4eddadc44237a7e57a0f9cb78a840d1d41", "txid": "755c820771284d85ea4bbcc246565b4eddadc44237a7e57a0f9cb78a840d1d41",
"vout": 0, "vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000 "amount": 1000
}, },
{ {
"txid": "66a0df86fcdeb84b8877adfe0b2c556dba30305d72ddbd4c49355f6930355357", "txid": "66a0df86fcdeb84b8877adfe0b2c556dba30305d72ddbd4c49355f6930355357",
"vout": 0, "vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000 "amount": 1000
}, },
{ {
"txid": "9913159bc7aa493ca53cbb9cbc88f97ba01137c814009dc7ef520c3fafc67909", "txid": "9913159bc7aa493ca53cbb9cbc88f97ba01137c814009dc7ef520c3fafc67909",
"vout": 1, "vout": 1,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 500 "amount": 500
}, },
{ {
"txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60", "txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60",
"vout": 0, "vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000 "amount": 1000
}, },
{ {
"txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60", "txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60",
"vout": 1, "vout": 1,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000 "amount": 1000
} }
], ],
"receivers": [ "receivers": [
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 1000 "amount": 1000
}, },
{ {
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000 "amount": 1000
}, },
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 1000 "amount": 1000
}, },
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 1000 "amount": 1000
}, },
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 500 "amount": 500
} }
] ]
@@ -197,53 +206,58 @@
{ {
"txid": "755c820771284d85ea4bbcc246565b4eddadc44237a7e57a0f9cb78a840d1d41", "txid": "755c820771284d85ea4bbcc246565b4eddadc44237a7e57a0f9cb78a840d1d41",
"vout": 0, "vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000 "amount": 1000
}, },
{ {
"txid": "66a0df86fcdeb84b8877adfe0b2c556dba30305d72ddbd4c49355f6930355357", "txid": "66a0df86fcdeb84b8877adfe0b2c556dba30305d72ddbd4c49355f6930355357",
"vout": 0, "vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000 "amount": 1000
}, },
{ {
"txid": "9913159bc7aa493ca53cbb9cbc88f97ba01137c814009dc7ef520c3fafc67909", "txid": "9913159bc7aa493ca53cbb9cbc88f97ba01137c814009dc7ef520c3fafc67909",
"vout": 1, "vout": 1,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 500 "amount": 500
}, },
{ {
"txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60", "txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60",
"vout": 0, "vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000 "amount": 1000
}, },
{ {
"txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60", "txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60",
"vout": 1, "vout": 1,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000 "amount": 1000
} }
], ],
"receivers": [ "receivers": [
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 1000 "amount": 1000
}, },
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 1000 "amount": 1000
}, },
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 1000 "amount": 1000
}, },
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 1000 "amount": 1000
}, },
{ {
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })", "pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 500 "amount": 500
} }
] ]
@@ -252,7 +266,7 @@
"poolTx": "cHNidP8BALICAAAAAnonOnsJBkHUUaKf/7fdS0/sVyBCgDPusYzGSZZiXPbtAAAAAAD/////VLtr81ZII3QJnXgrIwgcnbsq3aa4L3qdHOAn2evlFtEAAAAAAP////8CohIAAAAAAAAiUSBZarBUuSIHnlkuIoel9MmvexqTGK8jCZaRjt8L+Pb3s+gDAAAAAAAAIlEgI95L4kHEn2fAA+vysD+RIR4eD3AIQwc+FyCInJ8HivYAAAAAAAEBIOgDAAAAAAAAF6kU6p9IboLvs92Dpp/Zbj8BE3V9oDyHAAEBIOgDAAAAAAAAF6kU6p9IboLvs92Dpp/Zbj8BE3V9oDyHAAAA", "poolTx": "cHNidP8BALICAAAAAnonOnsJBkHUUaKf/7fdS0/sVyBCgDPusYzGSZZiXPbtAAAAAAD/////VLtr81ZII3QJnXgrIwgcnbsq3aa4L3qdHOAn2evlFtEAAAAAAP////8CohIAAAAAAAAiUSBZarBUuSIHnlkuIoel9MmvexqTGK8jCZaRjt8L+Pb3s+gDAAAAAAAAIlEgI95L4kHEn2fAA+vysD+RIR4eD3AIQwc+FyCInJ8HivYAAAAAAAEBIOgDAAAAAAAAF6kU6p9IboLvs92Dpp/Zbj8BE3V9oDyHAAEBIOgDAAAAAAAAF6kU6p9IboLvs92Dpp/Zbj8BE3V9oDyHAAAA",
"poolTxid": "7c0c10756cdb9ab8e605f1c82e25989761308cf4c60e6a6f42b72d46144c4ce0", "poolTxid": "7c0c10756cdb9ab8e605f1c82e25989761308cf4c60e6a6f42b72d46144c4ce0",
"expectedNumOfForfeitTxs": 25, "expectedNumOfForfeitTxs": 25,
"expectedNumOfConnectors": 4 "expectedNumOfConnectors": 4
} }
], ],
"invalid": [] "invalid": []

View File

@@ -1,47 +1,58 @@
package txbuilder package txbuilder
import ( import (
"github.com/ark-network/ark/common/bitcointree" "github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/server/internal/core/domain" "github.com/ark-network/ark/server/internal/core/domain"
"github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
) )
func getOnchainReceivers( func getOnchainOutputs(
payments []domain.Payment, payments []domain.Payment, network *chaincfg.Params,
) []domain.Receiver { ) ([]*wire.TxOut, error) {
receivers := make([]domain.Receiver, 0) outputs := make([]*wire.TxOut, 0)
for _, payment := range payments { for _, payment := range payments {
for _, receiver := range payment.Receivers { for _, receiver := range payment.Receivers {
if receiver.IsOnchain() { if receiver.IsOnchain() {
receivers = append(receivers, receiver) receiverAddr, err := btcutil.DecodeAddress(receiver.OnchainAddress, network)
}
}
}
return receivers
}
func getOffchainReceivers(
payments []domain.Payment,
) ([]bitcointree.Receiver, error) {
receivers := make([]bitcointree.Receiver, 0)
for _, payment := range payments {
for _, receiver := range payment.Receivers {
if !receiver.IsOnchain() {
vtxoScript, err := bitcointree.ParseVtxoScript(receiver.Descriptor)
if err != nil { if err != nil {
return nil, err return nil, err
} }
receivers = append(receivers, bitcointree.Receiver{ receiverScript, err := txscript.PayToAddrScript(receiverAddr)
Script: vtxoScript, if err != nil {
return nil, err
}
outputs = append(outputs, &wire.TxOut{
Value: int64(receiver.Amount),
PkScript: receiverScript,
})
}
}
}
return outputs, nil
}
func getOutputVtxosLeaves(
payments []domain.Payment,
) ([]tree.VtxoLeaf, error) {
leaves := make([]tree.VtxoLeaf, 0)
for _, payment := range payments {
for _, receiver := range payment.Receivers {
if !receiver.IsOnchain() {
leaves = append(leaves, tree.VtxoLeaf{
Pubkey: receiver.Pubkey,
Amount: receiver.Amount, Amount: receiver.Amount,
}) })
} }
} }
} }
return receivers, nil return leaves, nil
} }
func countSpentVtxos(payments []domain.Payment) uint64 { func countSpentVtxos(payments []domain.Payment) uint64 {

View File

@@ -338,7 +338,7 @@ func (h *handler) Ping(
func (h *handler) CreatePayment( func (h *handler) CreatePayment(
ctx context.Context, req *arkv1.CreatePaymentRequest, ctx context.Context, req *arkv1.CreatePaymentRequest,
) (*arkv1.CreatePaymentResponse, error) { ) (*arkv1.CreatePaymentResponse, error) {
inputs, err := parseInputs(req.GetInputs()) inputs, err := parseAsyncPaymentInputs(req.GetInputs())
if err != nil { if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error()) return nil, status.Error(codes.InvalidArgument, err.Error())
} }
@@ -353,12 +353,12 @@ func (h *handler) CreatePayment(
return nil, status.Error(codes.InvalidArgument, "output amount must be greater than 0") return nil, status.Error(codes.InvalidArgument, "output amount must be greater than 0")
} }
if len(receiver.OnchainAddress) > 0 { if len(receiver.OnchainAddress) <= 0 && len(receiver.Pubkey) <= 0 {
return nil, status.Error(codes.InvalidArgument, "onchain address is not supported as async payment destination") return nil, status.Error(codes.InvalidArgument, "missing address")
} }
if len(receiver.Descriptor) <= 0 { if receiver.IsOnchain() {
return nil, status.Error(codes.InvalidArgument, "missing output descriptor") return nil, status.Error(codes.InvalidArgument, "onchain outputs are not supported as async payment destination")
} }
} }
@@ -462,12 +462,12 @@ func (h *handler) GetRoundById(
func (h *handler) ListVtxos( func (h *handler) ListVtxos(
ctx context.Context, req *arkv1.ListVtxosRequest, ctx context.Context, req *arkv1.ListVtxosRequest,
) (*arkv1.ListVtxosResponse, error) { ) (*arkv1.ListVtxosResponse, error) {
_, userPubkey, _, err := parseAddress(req.GetAddress()) _, err := parseAddress(req.GetAddress())
if err != nil { if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error()) return nil, status.Error(codes.InvalidArgument, err.Error())
} }
spendableVtxos, spentVtxos, err := h.svc.ListVtxos(ctx, userPubkey) spendableVtxos, spentVtxos, err := h.svc.ListVtxos(ctx, req.GetAddress())
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -1,25 +1,55 @@
package handlers package handlers
import ( import (
"encoding/hex"
"fmt" "fmt"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1" arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/common" "github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/server/internal/core/application"
"github.com/ark-network/ark/server/internal/core/domain" "github.com/ark-network/ark/server/internal/core/domain"
"github.com/ark-network/ark/server/internal/core/ports" "github.com/ark-network/ark/server/internal/core/ports"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash"
) )
// From interface type to app type // From interface type to app type
func parseAddress(addr string) (string, *secp256k1.PublicKey, *secp256k1.PublicKey, error) { func parseAddress(addr string) (*common.Address, error) {
if len(addr) <= 0 { if len(addr) <= 0 {
return "", nil, nil, fmt.Errorf("missing address") return nil, fmt.Errorf("missing address")
} }
return common.DecodeAddress(addr) return common.DecodeAddress(addr)
} }
func parseAsyncPaymentInputs(ins []*arkv1.AsyncPaymentInput) ([]application.AsyncPaymentInput, error) {
if len(ins) <= 0 {
return nil, fmt.Errorf("missing inputs")
}
inputs := make([]application.AsyncPaymentInput, 0, len(ins))
for _, input := range ins {
forfeitLeafHash, err := chainhash.NewHashFromStr(input.GetForfeitLeafHash())
if err != nil {
return nil, fmt.Errorf("invalid forfeit leaf hash: %s", err)
}
inputs = append(inputs, application.AsyncPaymentInput{
Input: ports.Input{
VtxoKey: domain.VtxoKey{
Txid: input.GetInput().GetOutpoint().GetTxid(),
VOut: input.GetInput().GetOutpoint().GetVout(),
},
Descriptor: input.GetInput().GetDescriptor_(),
},
ForfeitLeafHash: *forfeitLeafHash,
})
}
return inputs, nil
}
func parseInputs(ins []*arkv1.Input) ([]ports.Input, error) { func parseInputs(ins []*arkv1.Input) ([]ports.Input, error) {
if len(ins) <= 0 { if len(ins) <= 0 {
return nil, fmt.Errorf("missing inputs") return nil, fmt.Errorf("missing inputs")
@@ -39,26 +69,43 @@ func parseInputs(ins []*arkv1.Input) ([]ports.Input, error) {
return inputs, nil return inputs, nil
} }
func parseReceiver(out *arkv1.Output) (domain.Receiver, error) {
decodedAddr, err := common.DecodeAddress(out.GetAddress())
if err != nil {
// onchain address
return domain.Receiver{
Amount: out.GetAmount(),
OnchainAddress: out.GetAddress(),
}, nil
}
return domain.Receiver{
Amount: out.GetAmount(),
Pubkey: hex.EncodeToString(schnorr.SerializePubKey(decodedAddr.VtxoTapKey)),
}, nil
}
func parseReceivers(outs []*arkv1.Output) ([]domain.Receiver, error) { func parseReceivers(outs []*arkv1.Output) ([]domain.Receiver, error) {
receivers := make([]domain.Receiver, 0, len(outs)) receivers := make([]domain.Receiver, 0, len(outs))
for _, out := range outs { for _, out := range outs {
if out.GetAmount() == 0 { if out.GetAmount() == 0 {
return nil, fmt.Errorf("missing output amount") return nil, fmt.Errorf("missing output amount")
} }
if len(out.GetAddress()) <= 0 && len(out.GetDescriptor_()) <= 0 { if len(out.GetAddress()) <= 0 {
return nil, fmt.Errorf("missing output destination") return nil, fmt.Errorf("missing output destination")
} }
receivers = append(receivers, domain.Receiver{ rcv, err := parseReceiver(out)
Descriptor: out.GetDescriptor_(), if err != nil {
Amount: out.GetAmount(), return nil, err
OnchainAddress: out.GetAddress(), }
})
receivers = append(receivers, rcv)
} }
return receivers, nil return receivers, nil
} }
// From app typeto interface type // From app type to interface type
type vtxoList []domain.Vtxo type vtxoList []domain.Vtxo
@@ -70,15 +117,15 @@ func (v vtxoList) toProto() []*arkv1.Vtxo {
Txid: vv.Txid, Txid: vv.Txid,
Vout: vv.VOut, Vout: vv.VOut,
}, },
Descriptor_: vv.Descriptor, Amount: vv.Amount,
Amount: vv.Amount, RoundTxid: vv.RoundTxid,
RoundTxid: vv.RoundTxid, Spent: vv.Spent,
Spent: vv.Spent, ExpireAt: vv.ExpireAt,
ExpireAt: vv.ExpireAt, SpentBy: vv.SpentBy,
SpentBy: vv.SpentBy, Swept: vv.Swept,
Swept: vv.Swept, RedeemTx: vv.RedeemTx,
RedeemTx: vv.RedeemTx, Pending: vv.Pending,
Pending: vv.Pending, Pubkey: vv.Pubkey,
}) })
} }