Add CreatePayment and CompletePayment (#229)

Co-authored-by: Marco Argentieri <tiero@users.noreply.github.com>

* Add claim command

* Persist pending data in sqlite repo

* Remove debug log

* Return pending data at interface level

* Fix unlocking btc wallet after restart

* Lint & Fix whitelist permissions

* Fix send command for covenant

* Update client/covenantless/claim.go

Signed-off-by: Marco Argentieri <3596602+tiero@users.noreply.github.com>

* Fix

* Pay for min relay fee instead of estimating fees for redeem and unconf forfeit txs

* Add support for pending payments (coventanless)

* Fixes

* Fixes

* Improve verbosity

* Fix coin selection

* Fix

---------

Signed-off-by: Marco Argentieri <3596602+tiero@users.noreply.github.com>
Co-authored-by: louisinger <louis@vulpem.com>
Co-authored-by: Marco Argentieri <tiero@users.noreply.github.com>
Co-authored-by: Marco Argentieri <3596602+tiero@users.noreply.github.com>
This commit is contained in:
Pietralberto Mazza
2024-08-10 19:18:02 +02:00
committed by GitHub
parent 57ce08f239
commit 72a7f29bab
67 changed files with 4730 additions and 769 deletions

View File

@@ -25,6 +25,14 @@ const dust = 450
type covenantLiquidCLI struct{} type covenantLiquidCLI struct{}
func (c *covenantLiquidCLI) SendAsync(ctx *cli.Context) error {
return fmt.Errorf("not implemented")
}
func (c *covenantLiquidCLI) ClaimAsync(ctx *cli.Context) error {
return fmt.Errorf("not implemented")
}
func (c *covenantLiquidCLI) Receive(ctx *cli.Context) error { func (c *covenantLiquidCLI) Receive(ctx *cli.Context) error {
offchainAddr, onchainAddr, _, err := getAddress(ctx) offchainAddr, onchainAddr, _, err := getAddress(ctx)
if err != nil { if err != nil {

View File

@@ -109,7 +109,7 @@ func getClient(addr string) (arkv1.ArkServiceClient, func(), error) {
if !strings.Contains(addr, ":") { if !strings.Contains(addr, ":") {
addr = fmt.Sprintf("%s:%d", addr, port) addr = fmt.Sprintf("%s:%d", addr, port)
} }
conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(creds)) conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(creds))
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }

View File

@@ -0,0 +1,98 @@
package covenantless
import (
"github.com/ark-network/ark-cli/utils"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/urfave/cli/v2"
)
func (c *clArkBitcoinCLI) ClaimAsync(ctx *cli.Context) error {
client, cancel, err := getClientFromState(ctx)
if err != nil {
return err
}
defer cancel()
myselfOffchain, _, _, err := getAddress(ctx)
if err != nil {
return err
}
vtxos, err := getVtxos(ctx, nil, client, myselfOffchain, false)
if err != nil {
return err
}
var pendingBalance uint64
var pendingVtxos []vtxo
for _, vtxo := range vtxos {
if vtxo.pending {
pendingBalance += vtxo.amount
pendingVtxos = append(pendingVtxos, vtxo)
}
}
if pendingBalance == 0 {
return nil
}
receiver := receiver{
To: myselfOffchain,
Amount: pendingBalance,
}
return selfTransferAllPendingPayments(
ctx, client, pendingVtxos, receiver,
)
}
func selfTransferAllPendingPayments(
ctx *cli.Context, client arkv1.ArkServiceClient,
pendingVtxos []vtxo, myself receiver,
) error {
inputs := make([]*arkv1.Input, 0, len(pendingVtxos))
for _, coin := range pendingVtxos {
inputs = append(inputs, &arkv1.Input{
Txid: coin.txid,
Vout: coin.vout,
})
}
receiversOutput := []*arkv1.Output{
{
Address: myself.To,
Amount: myself.Amount,
},
}
secKey, err := utils.PrivateKeyFromPassword(ctx)
if err != nil {
return err
}
registerResponse, err := client.RegisterPayment(
ctx.Context, &arkv1.RegisterPaymentRequest{Inputs: inputs},
)
if err != nil {
return err
}
_, err = client.ClaimPayment(ctx.Context, &arkv1.ClaimPaymentRequest{
Id: registerResponse.GetId(),
Outputs: []*arkv1.Output{{Address: myself.To, Amount: myself.Amount}},
})
if err != nil {
return err
}
poolTxID, err := handleRoundStream(
ctx, client, registerResponse.GetId(),
pendingVtxos, secKey, receiversOutput,
)
if err != nil {
return err
}
return utils.PrintJSON(map[string]interface{}{
"pool_txid": poolTxID,
})
}

View File

@@ -70,15 +70,19 @@ func New() interfaces.CLI {
return &clArkBitcoinCLI{} return &clArkBitcoinCLI{}
} }
func (c *clArkBitcoinCLI) Send(ctx *cli.Context) error {
return fmt.Errorf("not implemented")
}
type receiver struct { type receiver struct {
To string `json:"to"` To string `json:"to"`
Amount uint64 `json:"amount"` Amount uint64 `json:"amount"`
} }
func (r *receiver) isOnchain() bool { // func (r *receiver) isOnchain() bool {
_, err := btcutil.DecodeAddress(r.To, nil) // _, err := btcutil.DecodeAddress(r.To, nil)
return err == nil // return err == nil
} // }
func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) { func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
ptx, err := psbt.New(nil, nil, 2, 0, nil) ptx, err := psbt.New(nil, nil, 2, 0, nil)

View File

@@ -27,6 +27,7 @@ type vtxo struct {
vout uint32 vout uint32
poolTxid string poolTxid string
expireAt *time.Time expireAt *time.Time
pending bool
} }
func getVtxos( func getVtxos(
@@ -43,19 +44,20 @@ func getVtxos(
vtxos := make([]vtxo, 0, len(response.GetSpendableVtxos())) vtxos := make([]vtxo, 0, len(response.GetSpendableVtxos()))
for _, v := range response.GetSpendableVtxos() { for _, v := range response.GetSpendableVtxos() {
var expireAt *time.Time var expireAt *time.Time
if v.ExpireAt > 0 { if v.GetExpireAt() > 0 {
t := time.Unix(v.ExpireAt, 0) t := time.Unix(v.ExpireAt, 0)
expireAt = &t expireAt = &t
} }
if v.Swept { if v.GetSwept() {
continue continue
} }
vtxos = append(vtxos, vtxo{ vtxos = append(vtxos, vtxo{
amount: v.Receiver.Amount, amount: v.GetReceiver().GetAmount(),
txid: v.Outpoint.Txid, txid: v.GetOutpoint().GetTxid(),
vout: v.Outpoint.Vout, vout: v.GetOutpoint().GetVout(),
poolTxid: v.PoolTxid, poolTxid: v.GetPoolTxid(),
expireAt: expireAt, expireAt: expireAt,
pending: v.GetPending(),
}) })
} }
@@ -108,7 +110,7 @@ func getClient(addr string) (arkv1.ArkServiceClient, func(), error) {
if !strings.Contains(addr, ":") { if !strings.Contains(addr, ":") {
addr = fmt.Sprintf("%s:%d", addr, port) addr = fmt.Sprintf("%s:%d", addr, port)
} }
conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(creds)) conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(creds))
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }

View File

@@ -2,118 +2,63 @@ package covenantless
import ( import (
"bytes" "bytes"
"encoding/json"
"fmt" "fmt"
"sort" "sort"
"strings"
"github.com/ark-network/ark-cli/utils" "github.com/ark-network/ark-cli/utils"
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/btcsuite/btcd/btcutil/psbt"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
func (c *clArkBitcoinCLI) Send(ctx *cli.Context) error { func (c *clArkBitcoinCLI) SendAsync(ctx *cli.Context) error {
if !ctx.IsSet("receivers") && !ctx.IsSet("to") && !ctx.IsSet("amount") { receiver := ctx.String("to")
return fmt.Errorf("missing destination, either use --to and --amount to send or --receivers to send to many")
}
receivers := ctx.String("receivers")
to := ctx.String("to")
amount := ctx.Uint64("amount") amount := ctx.Uint64("amount")
var receiversJSON []receiver
if len(receivers) > 0 {
if err := json.Unmarshal([]byte(receivers), &receiversJSON); err != nil {
return fmt.Errorf("invalid receivers: %s", err)
}
} else {
receiversJSON = []receiver{
{
To: to,
Amount: amount,
},
}
}
if len(receiversJSON) <= 0 {
return fmt.Errorf("no receivers specified")
}
onchainReceivers := make([]receiver, 0)
offchainReceivers := make([]receiver, 0)
for _, receiver := range receiversJSON {
if receiver.isOnchain() {
onchainReceivers = append(onchainReceivers, receiver)
} else {
offchainReceivers = append(offchainReceivers, receiver)
}
}
explorer := utils.NewExplorer(ctx)
if len(onchainReceivers) > 0 {
pset, err := sendOnchain(ctx, onchainReceivers)
if err != nil {
return err
}
txid, err := explorer.Broadcast(pset)
if err != nil {
return err
}
return utils.PrintJSON(map[string]interface{}{
"txid": txid,
})
}
if len(offchainReceivers) > 0 {
if err := sendOffchain(ctx, offchainReceivers); err != nil {
return err
}
}
return nil
}
func sendOffchain(ctx *cli.Context, receivers []receiver) error {
withExpiryCoinselect := ctx.Bool("enable-expiry-coinselect") withExpiryCoinselect := ctx.Bool("enable-expiry-coinselect")
if amount < dust {
return fmt.Errorf("invalid amount (%d), must be greater than dust %d", amount, dust)
}
if receiver == "" {
return fmt.Errorf("receiver address is required")
}
isOnchain, _, _, err := decodeReceiverAddress(receiver)
if err != nil {
return err
}
if isOnchain {
return fmt.Errorf("receiver address is onchain")
}
offchainAddr, _, _, err := getAddress(ctx) offchainAddr, _, _, err := getAddress(ctx)
if err != nil { if err != nil {
return err return err
} }
_, _, aspPubKey, err := common.DecodeAddress(offchainAddr) _, _, aspPubKey, err := common.DecodeAddress(offchainAddr)
if err != nil { if err != nil {
return err return err
} }
_, _, aspKey, err := common.DecodeAddress(receiver)
if err != nil {
return fmt.Errorf("invalid receiver address: %s", err)
}
if !bytes.Equal(
aspPubKey.SerializeCompressed(), aspKey.SerializeCompressed(),
) {
return fmt.Errorf("invalid receiver address '%s': must be associated with the connected service provider", receiver)
}
receiversOutput := make([]*arkv1.Output, 0) receiversOutput := make([]*arkv1.Output, 0)
sumOfReceivers := uint64(0) sumOfReceivers := uint64(0)
receiversOutput = append(receiversOutput, &arkv1.Output{
Address: receiver,
Amount: amount,
})
sumOfReceivers += amount
for _, receiver := range receivers {
_, _, aspKey, err := common.DecodeAddress(receiver.To)
if err != nil {
return fmt.Errorf("invalid receiver address: %s", err)
}
if !bytes.Equal(
aspPubKey.SerializeCompressed(), aspKey.SerializeCompressed(),
) {
return fmt.Errorf("invalid receiver address '%s': must be associated with the connected service provider", receiver.To)
}
if receiver.Amount < dust {
return fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount, dust)
}
receiversOutput = append(receiversOutput, &arkv1.Output{
Address: receiver.To,
Amount: uint64(receiver.Amount),
})
sumOfReceivers += receiver.Amount
}
client, close, err := getClientFromState(ctx) client, close, err := getClientFromState(ctx)
if err != nil { if err != nil {
return err return err
@@ -148,37 +93,66 @@ func sendOffchain(ctx *cli.Context, receivers []receiver) error {
}) })
} }
secKey, err := utils.PrivateKeyFromPassword(ctx) resp, err := client.CreatePayment(
ctx.Context, &arkv1.CreatePaymentRequest{
Inputs: inputs,
Outputs: receiversOutput,
})
if err != nil { if err != nil {
return err return err
} }
registerResponse, err := client.RegisterPayment( // TODO verify the redeem tx signature
ctx.Context, &arkv1.RegisterPaymentRequest{Inputs: inputs}, fmt.Println("payment created")
) fmt.Println("signing redeem and forfeit txs...")
seckey, err := utils.PrivateKeyFromPassword(ctx)
if err != nil { if err != nil {
return err return err
} }
_, err = client.ClaimPayment(ctx.Context, &arkv1.ClaimPaymentRequest{ signedUnconditionalForfeitTxs := make([]string, 0, len(resp.UsignedUnconditionalForfeitTxs))
Id: registerResponse.GetId(), for _, tx := range resp.UsignedUnconditionalForfeitTxs {
Outputs: receiversOutput, forfeitPtx, err := psbt.NewFromRawBytes(strings.NewReader(tx), true)
}) if err != nil {
return err
}
if err := signPsbt(ctx, forfeitPtx, explorer, seckey); err != nil {
return err
}
signedForfeitTx, err := forfeitPtx.B64Encode()
if err != nil {
return err
}
signedUnconditionalForfeitTxs = append(signedUnconditionalForfeitTxs, signedForfeitTx)
}
redeemPtx, err := psbt.NewFromRawBytes(strings.NewReader(resp.SignedRedeemTx), true)
if err != nil { if err != nil {
return err return err
} }
poolTxID, err := handleRoundStream( if err := signPsbt(ctx, redeemPtx, explorer, seckey); err != nil {
ctx, client, registerResponse.GetId(), return err
selectedCoins, secKey, receiversOutput, }
)
signedRedeem, err := redeemPtx.B64Encode()
if err != nil { if err != nil {
return err return err
} }
return utils.PrintJSON(map[string]interface{}{ if _, err = client.CompletePayment(ctx.Context, &arkv1.CompletePaymentRequest{
"pool_txid": poolTxID, SignedRedeemTx: signedRedeem,
}) SignedUnconditionalForfeitTxs: signedUnconditionalForfeitTxs,
}); err != nil {
return err
}
fmt.Println("payment completed")
return nil
} }
func coinSelect(vtxos []vtxo, amount uint64, sortByExpirationTime bool) ([]vtxo, uint64, error) { func coinSelect(vtxos []vtxo, amount uint64, sortByExpirationTime bool) ([]vtxo, uint64, error) {
@@ -213,7 +187,7 @@ func coinSelect(vtxos []vtxo, amount uint64, sortByExpirationTime bool) ([]vtxo,
change := selectedAmount - amount change := selectedAmount - amount
if change < dust { if change > 0 && change < dust {
if len(notSelected) > 0 { if len(notSelected) > 0 {
selected = append(selected, notSelected[0]) selected = append(selected, notSelected[0])
change += notSelected[0].amount change += notSelected[0].amount

View File

@@ -132,7 +132,7 @@ func signPsbt(
switch c := closure.(type) { switch c := closure.(type) {
case *bitcointree.CSVSigClosure: case *bitcointree.CSVSigClosure:
sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], pubkey.SerializeCompressed()[1:]) sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], pubkey.SerializeCompressed()[1:])
case *bitcointree.ForfeitClosure: case *bitcointree.MultisigClosure:
sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], pubkey.SerializeCompressed()[1:]) sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], pubkey.SerializeCompressed()[1:])
} }

View File

@@ -14,7 +14,7 @@ func computeVtxoTaprootScript(
Seconds: exitDelay, Seconds: exitDelay,
} }
forfeitClosure := &bitcointree.ForfeitClosure{ forfeitClosure := &bitcointree.MultisigClosure{
Pubkey: userPubkey, Pubkey: userPubkey,
AspPubkey: aspPubkey, AspPubkey: aspPubkey,
} }

View File

@@ -72,18 +72,22 @@ var (
Value: "", Value: "",
Required: false, Required: false,
} }
AmountToRedeemFlag = cli.Uint64Flag{ AmountToRedeemFlag = cli.Uint64Flag{
Name: "amount", Name: "amount",
Usage: "amount to redeem", Usage: "amount to redeem",
Value: 0, Value: 0,
Required: false, Required: false,
} }
ForceFlag = cli.BoolFlag{ ForceFlag = cli.BoolFlag{
Name: "force", Name: "force",
Usage: "force redemption without collaborate with the Ark service provider", Usage: "force redemption without collaborate with the Ark service provider",
Value: false, Value: false,
Required: false, Required: false,
} }
AsyncPaymentFlag = cli.BoolFlag{
Name: "async",
Usage: "use async payment protocol",
Value: false,
Required: false,
}
) )

View File

@@ -8,5 +8,7 @@ type CLI interface {
Receive(ctx *cli.Context) error Receive(ctx *cli.Context) error
Redeem(ctx *cli.Context) error Redeem(ctx *cli.Context) error
Send(ctx *cli.Context) error Send(ctx *cli.Context) error
ClaimAsync(ctx *cli.Context) error
SendAsync(ctx *cli.Context) error
Onboard(ctx *cli.Context) error Onboard(ctx *cli.Context) error
} }

View File

@@ -92,14 +92,36 @@ var (
sendCommand = cli.Command{ sendCommand = cli.Command{
Name: "send", Name: "send",
Usage: "Send your onchain or offchain funds to one or many receivers", Usage: "Send your onchain or offchain funds to one or many receivers",
Action: func(ctx *cli.Context) error {
state, err := utils.GetState(ctx)
if err != nil {
return err
}
networkName := state[utils.NETWORK]
cli, err := getCLI(networkName)
if err != nil {
return err
}
if strings.Contains(networkName, "liquid") {
return cli.Send(ctx)
}
return cli.SendAsync(ctx)
},
Flags: []cli.Flag{&flags.ReceiversFlag, &flags.ToFlag, &flags.AmountFlag, &flags.PasswordFlag, &flags.EnableExpiryCoinselectFlag, &flags.AsyncPaymentFlag},
}
claimCommand = cli.Command{
Name: "claim",
Usage: "Join round to claim pending payments",
Action: func(ctx *cli.Context) error { Action: func(ctx *cli.Context) error {
cli, err := getCLIFromState(ctx) cli, err := getCLIFromState(ctx)
if err != nil { if err != nil {
return err return err
} }
return cli.Send(ctx) return cli.ClaimAsync(ctx)
}, },
Flags: []cli.Flag{&flags.ReceiversFlag, &flags.ToFlag, &flags.AmountFlag, &flags.PasswordFlag, &flags.EnableExpiryCoinselectFlag}, Flags: []cli.Flag{&flags.PasswordFlag},
} }
receiveCommand = cli.Command{ receiveCommand = cli.Command{
@@ -144,6 +166,7 @@ func main() {
&receiveCommand, &receiveCommand,
&redeemCommand, &redeemCommand,
&sendCommand, &sendCommand,
&claimCommand,
&onboardCommand, &onboardCommand,
) )
app.Flags = []cli.Flag{ app.Flags = []cli.Flag{

View File

@@ -84,7 +84,6 @@ func (e *explorer) GetFeeRate() (float64, error) {
fmt.Println("empty fee-estimates response, default to 2 sat/vbyte") fmt.Println("empty fee-estimates response, default to 2 sat/vbyte")
return 2, nil return 2, nil
} }
fmt.Println("fee rate", response["1"])
return response["1"], nil return response["1"], nil
} }

View File

@@ -155,7 +155,7 @@ func (l *leaf) getOutputs() ([]*wire.TxOut, error) {
return nil, err return nil, err
} }
forfeitClosure := &ForfeitClosure{ forfeitClosure := &MultisigClosure{
Pubkey: l.vtxoKey, Pubkey: l.vtxoKey,
AspPubkey: l.aspKey, AspPubkey: l.aspKey,
} }

View File

@@ -20,7 +20,7 @@ type CSVSigClosure struct {
Seconds uint Seconds uint
} }
type ForfeitClosure struct { type MultisigClosure struct {
Pubkey *secp256k1.PublicKey Pubkey *secp256k1.PublicKey
AspPubkey *secp256k1.PublicKey AspPubkey *secp256k1.PublicKey
} }
@@ -33,7 +33,7 @@ func DecodeClosure(script []byte) (Closure, error) {
return closure, nil return closure, nil
} }
closure = &ForfeitClosure{} closure = &MultisigClosure{}
if valid, err := closure.Decode(script); err == nil && valid { if valid, err := closure.Decode(script); err == nil && valid {
return closure, nil return closure, nil
} }
@@ -42,7 +42,7 @@ func DecodeClosure(script []byte) (Closure, error) {
} }
func (f *ForfeitClosure) Leaf() (*txscript.TapLeaf, error) { func (f *MultisigClosure) Leaf() (*txscript.TapLeaf, error) {
aspKeyBytes := schnorr.SerializePubKey(f.AspPubkey) aspKeyBytes := schnorr.SerializePubKey(f.AspPubkey)
userKeyBytes := schnorr.SerializePubKey(f.Pubkey) userKeyBytes := schnorr.SerializePubKey(f.Pubkey)
@@ -57,7 +57,7 @@ func (f *ForfeitClosure) Leaf() (*txscript.TapLeaf, error) {
return &tapLeaf, nil return &tapLeaf, nil
} }
func (f *ForfeitClosure) Decode(script []byte) (bool, error) { func (f *MultisigClosure) Decode(script []byte) (bool, error) {
valid, aspPubKey, err := decodeChecksigScript(script) valid, aspPubKey, err := decodeChecksigScript(script)
if err != nil { if err != nil {
return false, err return false, err
@@ -149,7 +149,7 @@ func ComputeVtxoTaprootScript(
Seconds: exitDelay, Seconds: exitDelay,
} }
forfeitClosure := &ForfeitClosure{ forfeitClosure := &MultisigClosure{
Pubkey: userPubkey, Pubkey: userPubkey,
AspPubkey: aspPubkey, AspPubkey: aspPubkey,
} }

View File

@@ -18,6 +18,9 @@ services:
- ARK_NO_MACAROONS=true - ARK_NO_MACAROONS=true
ports: ports:
- "6000:6000" - "6000:6000"
volumes:
- clarkd:/app/data
- clark:/app/wallet-data
volumes: volumes:
clarkd: clarkd:

View File

@@ -14,6 +14,9 @@ services:
- OCEAN_DB_TYPE=badger - OCEAN_DB_TYPE=badger
ports: ports:
- "18000:18000" - "18000:18000"
volumes:
- oceand:/app/data/oceand
- ocean:/app/data/ocean
arkd: arkd:
container_name: arkd container_name: arkd
build: build:
@@ -35,6 +38,9 @@ services:
- ARK_NO_MACAROONS=true - ARK_NO_MACAROONS=true
ports: ports:
- "8080:8080" - "8080:8080"
volumes:
- arkd:/app/data
- ark:/app/wallet-data
volumes: volumes:
oceand: oceand:

View File

@@ -23,6 +23,8 @@ type ArkClient interface {
CollaborativeRedeem( CollaborativeRedeem(
ctx context.Context, addr string, amount uint64, withExpiryCoinselect bool, ctx context.Context, addr string, amount uint64, withExpiryCoinselect bool,
) (string, error) ) (string, error)
SendAsync(ctx context.Context, withExpiryCoinselect bool, receivers []Receiver) (string, error)
ClaimAsync(ctx context.Context) (string, error)
} }
type Receiver interface { type Receiver interface {

View File

@@ -37,6 +37,12 @@ type ASPClient interface {
FinalizePayment( FinalizePayment(
ctx context.Context, signedForfeitTxs []string, ctx context.Context, signedForfeitTxs []string,
) error ) error
CreatePayment(
ctx context.Context, inputs []VtxoKey, outputs []Output,
) (string, []string, error)
CompletePayment(
ctx context.Context, signedRedeemTx string, signedUnconditionalForfeitTxs []string,
) error
Close() Close()
} }
@@ -61,9 +67,12 @@ type VtxoKey struct {
type Vtxo struct { type Vtxo struct {
VtxoKey VtxoKey
Amount uint64 Amount uint64
RoundTxid string RoundTxid string
ExpiresAt *time.Time ExpiresAt *time.Time
RedeemTx string
UnconditionalForfeitTxs []string
Pending bool
} }
type Output struct { type Output struct {

View File

@@ -199,6 +199,31 @@ func (a *grpcClient) FinalizePayment(
return err return err
} }
func (a *grpcClient) CreatePayment(
ctx context.Context, inputs []client.VtxoKey, outputs []client.Output,
) (string, []string, error) {
req := &arkv1.CreatePaymentRequest{
Inputs: ins(inputs).toProto(),
Outputs: outs(outputs).toProto(),
}
resp, err := a.svc.CreatePayment(ctx, req)
if err != nil {
return "", nil, err
}
return resp.SignedRedeemTx, resp.UsignedUnconditionalForfeitTxs, nil
}
func (a *grpcClient) CompletePayment(
ctx context.Context, redeemTx string, signedForfeitTxs []string,
) error {
req := &arkv1.CompletePaymentRequest{
SignedRedeemTx: redeemTx,
SignedUnconditionalForfeitTxs: signedForfeitTxs,
}
_, err := a.svc.CompletePayment(ctx, req)
return err
}
func (a *grpcClient) GetRoundByID( func (a *grpcClient) GetRoundByID(
ctx context.Context, roundID string, ctx context.Context, roundID string,
) (*client.Round, error) { ) (*client.Round, error) {
@@ -284,14 +309,23 @@ func (v vtxo) toVtxo() client.Vtxo {
t := time.Unix(v.GetExpireAt(), 0) t := time.Unix(v.GetExpireAt(), 0)
expiresAt = &t expiresAt = &t
} }
var redeemTx string
var uncondForfeitTxs []string
if v.GetPendingData() != nil {
redeemTx = v.GetPendingData().GetRedeemTx()
uncondForfeitTxs = v.GetPendingData().GetUnconditionalForfeitTxs()
}
return client.Vtxo{ return client.Vtxo{
VtxoKey: client.VtxoKey{ VtxoKey: client.VtxoKey{
Txid: v.GetOutpoint().GetTxid(), Txid: v.GetOutpoint().GetTxid(),
VOut: v.GetOutpoint().GetVout(), VOut: v.GetOutpoint().GetVout(),
}, },
Amount: v.GetReceiver().GetAmount(), Amount: v.GetReceiver().GetAmount(),
RoundTxid: v.GetPoolTxid(), RoundTxid: v.GetPoolTxid(),
ExpiresAt: expiresAt, ExpiresAt: expiresAt,
Pending: v.GetPending(),
RedeemTx: redeemTx,
UnconditionalForfeitTxs: uncondForfeitTxs,
} }
} }

View File

@@ -180,14 +180,24 @@ func (a *restClient) ListVtxos(
return nil, nil, err return nil, nil, err
} }
var redeemTx string
var uncondForfeitTxs []string
if v.PendingData != nil {
redeemTx = v.PendingData.RedeemTx
uncondForfeitTxs = v.PendingData.UnconditionalForfeitTxs
}
spendableVtxos = append(spendableVtxos, client.Vtxo{ spendableVtxos = append(spendableVtxos, client.Vtxo{
VtxoKey: client.VtxoKey{ VtxoKey: client.VtxoKey{
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.PoolTxid, RoundTxid: v.PoolTxid,
ExpiresAt: expiresAt, ExpiresAt: expiresAt,
Pending: v.Pending,
RedeemTx: redeemTx,
UnconditionalForfeitTxs: uncondForfeitTxs,
}) })
} }
@@ -358,6 +368,53 @@ func (a *restClient) FinalizePayment(
return err return err
} }
func (a *restClient) CreatePayment(
ctx context.Context, inputs []client.VtxoKey, outputs []client.Output,
) (string, []string, error) {
ins := make([]*models.V1Input, 0, len(inputs))
for _, i := range inputs {
ins = append(ins, &models.V1Input{
Txid: i.Txid,
Vout: int64(i.VOut),
})
}
outs := make([]*models.V1Output, 0, len(outputs))
for _, o := range outputs {
outs = append(outs, &models.V1Output{
Address: o.Address,
Amount: strconv.Itoa(int(o.Amount)),
})
}
body := models.V1CreatePaymentRequest{
Inputs: ins,
Outputs: outs,
}
resp, err := a.svc.ArkServiceCreatePayment(
ark_service.NewArkServiceCreatePaymentParams().WithBody(&body),
)
if err != nil {
return "", nil, err
}
return resp.GetPayload().SignedRedeemTx, resp.GetPayload().UsignedUnconditionalForfeitTxs, nil
}
func (a *restClient) CompletePayment(
ctx context.Context, signedRedeemTx string, signedUnconditionalForfeitTxs []string,
) error {
req := &arkv1.CompletePaymentRequest{
SignedRedeemTx: signedRedeemTx,
SignedUnconditionalForfeitTxs: signedUnconditionalForfeitTxs,
}
body := models.V1CompletePaymentRequest{
SignedRedeemTx: req.GetSignedRedeemTx(),
SignedUnconditionalForfeitTxs: req.GetSignedUnconditionalForfeitTxs(),
}
_, err := a.svc.ArkServiceCompletePayment(
ark_service.NewArkServiceCompletePaymentParams().WithBody(&body),
)
return err
}
func (a *restClient) GetRoundByID( func (a *restClient) GetRoundByID(
ctx context.Context, roundID string, ctx context.Context, roundID string,
) (*client.Round, error) { ) (*client.Round, error) {

View File

@@ -56,6 +56,10 @@ type ClientOption func(*runtime.ClientOperation)
type ClientService interface { type ClientService interface {
ArkServiceClaimPayment(params *ArkServiceClaimPaymentParams, opts ...ClientOption) (*ArkServiceClaimPaymentOK, error) ArkServiceClaimPayment(params *ArkServiceClaimPaymentParams, opts ...ClientOption) (*ArkServiceClaimPaymentOK, error)
ArkServiceCompletePayment(params *ArkServiceCompletePaymentParams, opts ...ClientOption) (*ArkServiceCompletePaymentOK, error)
ArkServiceCreatePayment(params *ArkServiceCreatePaymentParams, opts ...ClientOption) (*ArkServiceCreatePaymentOK, error)
ArkServiceFinalizePayment(params *ArkServiceFinalizePaymentParams, opts ...ClientOption) (*ArkServiceFinalizePaymentOK, error) ArkServiceFinalizePayment(params *ArkServiceFinalizePaymentParams, opts ...ClientOption) (*ArkServiceFinalizePaymentOK, error)
ArkServiceGetEventStream(params *ArkServiceGetEventStreamParams, opts ...ClientOption) (*ArkServiceGetEventStreamOK, error) ArkServiceGetEventStream(params *ArkServiceGetEventStreamParams, opts ...ClientOption) (*ArkServiceGetEventStreamOK, error)
@@ -114,6 +118,80 @@ func (a *Client) ArkServiceClaimPayment(params *ArkServiceClaimPaymentParams, op
return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code()) return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code())
} }
/*
ArkServiceCompletePayment ark service complete payment API
*/
func (a *Client) ArkServiceCompletePayment(params *ArkServiceCompletePaymentParams, opts ...ClientOption) (*ArkServiceCompletePaymentOK, error) {
// TODO: Validate the params before sending
if params == nil {
params = NewArkServiceCompletePaymentParams()
}
op := &runtime.ClientOperation{
ID: "ArkService_CompletePayment",
Method: "POST",
PathPattern: "/v1/payment/complete",
ProducesMediaTypes: []string{"application/json"},
ConsumesMediaTypes: []string{"application/json"},
Schemes: []string{"http"},
Params: params,
Reader: &ArkServiceCompletePaymentReader{formats: a.formats},
Context: params.Context,
Client: params.HTTPClient,
}
for _, opt := range opts {
opt(op)
}
result, err := a.transport.Submit(op)
if err != nil {
return nil, err
}
success, ok := result.(*ArkServiceCompletePaymentOK)
if ok {
return success, nil
}
// unexpected success response
unexpectedSuccess := result.(*ArkServiceCompletePaymentDefault)
return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code())
}
/*
ArkServiceCreatePayment ark service create payment API
*/
func (a *Client) ArkServiceCreatePayment(params *ArkServiceCreatePaymentParams, opts ...ClientOption) (*ArkServiceCreatePaymentOK, error) {
// TODO: Validate the params before sending
if params == nil {
params = NewArkServiceCreatePaymentParams()
}
op := &runtime.ClientOperation{
ID: "ArkService_CreatePayment",
Method: "POST",
PathPattern: "/v1/payment",
ProducesMediaTypes: []string{"application/json"},
ConsumesMediaTypes: []string{"application/json"},
Schemes: []string{"http"},
Params: params,
Reader: &ArkServiceCreatePaymentReader{formats: a.formats},
Context: params.Context,
Client: params.HTTPClient,
}
for _, opt := range opts {
opt(op)
}
result, err := a.transport.Submit(op)
if err != nil {
return nil, err
}
success, ok := result.(*ArkServiceCreatePaymentOK)
if ok {
return success, nil
}
// unexpected success response
unexpectedSuccess := result.(*ArkServiceCreatePaymentDefault)
return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code())
}
/* /*
ArkServiceFinalizePayment ark service finalize payment API ArkServiceFinalizePayment ark service finalize payment API
*/ */

View File

@@ -0,0 +1,150 @@
// Code generated by go-swagger; DO NOT EDIT.
package ark_service
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
"net/http"
"time"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
cr "github.com/go-openapi/runtime/client"
"github.com/go-openapi/strfmt"
"github.com/ark-network/ark/pkg/client-sdk/client/rest/service/models"
)
// NewArkServiceCompletePaymentParams creates a new ArkServiceCompletePaymentParams object,
// with the default timeout for this client.
//
// Default values are not hydrated, since defaults are normally applied by the API server side.
//
// To enforce default values in parameter, use SetDefaults or WithDefaults.
func NewArkServiceCompletePaymentParams() *ArkServiceCompletePaymentParams {
return &ArkServiceCompletePaymentParams{
timeout: cr.DefaultTimeout,
}
}
// NewArkServiceCompletePaymentParamsWithTimeout creates a new ArkServiceCompletePaymentParams object
// with the ability to set a timeout on a request.
func NewArkServiceCompletePaymentParamsWithTimeout(timeout time.Duration) *ArkServiceCompletePaymentParams {
return &ArkServiceCompletePaymentParams{
timeout: timeout,
}
}
// NewArkServiceCompletePaymentParamsWithContext creates a new ArkServiceCompletePaymentParams object
// with the ability to set a context for a request.
func NewArkServiceCompletePaymentParamsWithContext(ctx context.Context) *ArkServiceCompletePaymentParams {
return &ArkServiceCompletePaymentParams{
Context: ctx,
}
}
// NewArkServiceCompletePaymentParamsWithHTTPClient creates a new ArkServiceCompletePaymentParams object
// with the ability to set a custom HTTPClient for a request.
func NewArkServiceCompletePaymentParamsWithHTTPClient(client *http.Client) *ArkServiceCompletePaymentParams {
return &ArkServiceCompletePaymentParams{
HTTPClient: client,
}
}
/*
ArkServiceCompletePaymentParams contains all the parameters to send to the API endpoint
for the ark service complete payment operation.
Typically these are written to a http.Request.
*/
type ArkServiceCompletePaymentParams struct {
// Body.
Body *models.V1CompletePaymentRequest
timeout time.Duration
Context context.Context
HTTPClient *http.Client
}
// WithDefaults hydrates default values in the ark service complete payment params (not the query body).
//
// All values with no default are reset to their zero value.
func (o *ArkServiceCompletePaymentParams) WithDefaults() *ArkServiceCompletePaymentParams {
o.SetDefaults()
return o
}
// SetDefaults hydrates default values in the ark service complete payment params (not the query body).
//
// All values with no default are reset to their zero value.
func (o *ArkServiceCompletePaymentParams) SetDefaults() {
// no default values defined for this parameter
}
// WithTimeout adds the timeout to the ark service complete payment params
func (o *ArkServiceCompletePaymentParams) WithTimeout(timeout time.Duration) *ArkServiceCompletePaymentParams {
o.SetTimeout(timeout)
return o
}
// SetTimeout adds the timeout to the ark service complete payment params
func (o *ArkServiceCompletePaymentParams) SetTimeout(timeout time.Duration) {
o.timeout = timeout
}
// WithContext adds the context to the ark service complete payment params
func (o *ArkServiceCompletePaymentParams) WithContext(ctx context.Context) *ArkServiceCompletePaymentParams {
o.SetContext(ctx)
return o
}
// SetContext adds the context to the ark service complete payment params
func (o *ArkServiceCompletePaymentParams) SetContext(ctx context.Context) {
o.Context = ctx
}
// WithHTTPClient adds the HTTPClient to the ark service complete payment params
func (o *ArkServiceCompletePaymentParams) WithHTTPClient(client *http.Client) *ArkServiceCompletePaymentParams {
o.SetHTTPClient(client)
return o
}
// SetHTTPClient adds the HTTPClient to the ark service complete payment params
func (o *ArkServiceCompletePaymentParams) SetHTTPClient(client *http.Client) {
o.HTTPClient = client
}
// WithBody adds the body to the ark service complete payment params
func (o *ArkServiceCompletePaymentParams) WithBody(body *models.V1CompletePaymentRequest) *ArkServiceCompletePaymentParams {
o.SetBody(body)
return o
}
// SetBody adds the body to the ark service complete payment params
func (o *ArkServiceCompletePaymentParams) SetBody(body *models.V1CompletePaymentRequest) {
o.Body = body
}
// WriteToRequest writes these params to a swagger request
func (o *ArkServiceCompletePaymentParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error {
if err := r.SetTimeout(o.timeout); err != nil {
return err
}
var res []error
if o.Body != nil {
if err := r.SetBodyParam(o.Body); err != nil {
return err
}
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}

View File

@@ -0,0 +1,185 @@
// Code generated by go-swagger; DO NOT EDIT.
package ark_service
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"encoding/json"
"fmt"
"io"
"github.com/go-openapi/runtime"
"github.com/go-openapi/strfmt"
"github.com/ark-network/ark/pkg/client-sdk/client/rest/service/models"
)
// ArkServiceCompletePaymentReader is a Reader for the ArkServiceCompletePayment structure.
type ArkServiceCompletePaymentReader struct {
formats strfmt.Registry
}
// ReadResponse reads a server response into the received o.
func (o *ArkServiceCompletePaymentReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
switch response.Code() {
case 200:
result := NewArkServiceCompletePaymentOK()
if err := result.readResponse(response, consumer, o.formats); err != nil {
return nil, err
}
return result, nil
default:
result := NewArkServiceCompletePaymentDefault(response.Code())
if err := result.readResponse(response, consumer, o.formats); err != nil {
return nil, err
}
if response.Code()/100 == 2 {
return result, nil
}
return nil, result
}
}
// NewArkServiceCompletePaymentOK creates a ArkServiceCompletePaymentOK with default headers values
func NewArkServiceCompletePaymentOK() *ArkServiceCompletePaymentOK {
return &ArkServiceCompletePaymentOK{}
}
/*
ArkServiceCompletePaymentOK describes a response with status code 200, with default header values.
A successful response.
*/
type ArkServiceCompletePaymentOK struct {
Payload models.V1CompletePaymentResponse
}
// IsSuccess returns true when this ark service complete payment o k response has a 2xx status code
func (o *ArkServiceCompletePaymentOK) IsSuccess() bool {
return true
}
// IsRedirect returns true when this ark service complete payment o k response has a 3xx status code
func (o *ArkServiceCompletePaymentOK) IsRedirect() bool {
return false
}
// IsClientError returns true when this ark service complete payment o k response has a 4xx status code
func (o *ArkServiceCompletePaymentOK) IsClientError() bool {
return false
}
// IsServerError returns true when this ark service complete payment o k response has a 5xx status code
func (o *ArkServiceCompletePaymentOK) IsServerError() bool {
return false
}
// IsCode returns true when this ark service complete payment o k response a status code equal to that given
func (o *ArkServiceCompletePaymentOK) IsCode(code int) bool {
return code == 200
}
// Code gets the status code for the ark service complete payment o k response
func (o *ArkServiceCompletePaymentOK) Code() int {
return 200
}
func (o *ArkServiceCompletePaymentOK) Error() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[POST /v1/payment/complete][%d] arkServiceCompletePaymentOK %s", 200, payload)
}
func (o *ArkServiceCompletePaymentOK) String() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[POST /v1/payment/complete][%d] arkServiceCompletePaymentOK %s", 200, payload)
}
func (o *ArkServiceCompletePaymentOK) GetPayload() models.V1CompletePaymentResponse {
return o.Payload
}
func (o *ArkServiceCompletePaymentOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
// response payload
if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF {
return err
}
return nil
}
// NewArkServiceCompletePaymentDefault creates a ArkServiceCompletePaymentDefault with default headers values
func NewArkServiceCompletePaymentDefault(code int) *ArkServiceCompletePaymentDefault {
return &ArkServiceCompletePaymentDefault{
_statusCode: code,
}
}
/*
ArkServiceCompletePaymentDefault describes a response with status code -1, with default header values.
An unexpected error response.
*/
type ArkServiceCompletePaymentDefault struct {
_statusCode int
Payload *models.RPCStatus
}
// IsSuccess returns true when this ark service complete payment default response has a 2xx status code
func (o *ArkServiceCompletePaymentDefault) IsSuccess() bool {
return o._statusCode/100 == 2
}
// IsRedirect returns true when this ark service complete payment default response has a 3xx status code
func (o *ArkServiceCompletePaymentDefault) IsRedirect() bool {
return o._statusCode/100 == 3
}
// IsClientError returns true when this ark service complete payment default response has a 4xx status code
func (o *ArkServiceCompletePaymentDefault) IsClientError() bool {
return o._statusCode/100 == 4
}
// IsServerError returns true when this ark service complete payment default response has a 5xx status code
func (o *ArkServiceCompletePaymentDefault) IsServerError() bool {
return o._statusCode/100 == 5
}
// IsCode returns true when this ark service complete payment default response a status code equal to that given
func (o *ArkServiceCompletePaymentDefault) IsCode(code int) bool {
return o._statusCode == code
}
// Code gets the status code for the ark service complete payment default response
func (o *ArkServiceCompletePaymentDefault) Code() int {
return o._statusCode
}
func (o *ArkServiceCompletePaymentDefault) Error() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[POST /v1/payment/complete][%d] ArkService_CompletePayment default %s", o._statusCode, payload)
}
func (o *ArkServiceCompletePaymentDefault) String() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[POST /v1/payment/complete][%d] ArkService_CompletePayment default %s", o._statusCode, payload)
}
func (o *ArkServiceCompletePaymentDefault) GetPayload() *models.RPCStatus {
return o.Payload
}
func (o *ArkServiceCompletePaymentDefault) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
o.Payload = new(models.RPCStatus)
// response payload
if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
return err
}
return nil
}

View File

@@ -0,0 +1,150 @@
// Code generated by go-swagger; DO NOT EDIT.
package ark_service
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
"net/http"
"time"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
cr "github.com/go-openapi/runtime/client"
"github.com/go-openapi/strfmt"
"github.com/ark-network/ark/pkg/client-sdk/client/rest/service/models"
)
// NewArkServiceCreatePaymentParams creates a new ArkServiceCreatePaymentParams object,
// with the default timeout for this client.
//
// Default values are not hydrated, since defaults are normally applied by the API server side.
//
// To enforce default values in parameter, use SetDefaults or WithDefaults.
func NewArkServiceCreatePaymentParams() *ArkServiceCreatePaymentParams {
return &ArkServiceCreatePaymentParams{
timeout: cr.DefaultTimeout,
}
}
// NewArkServiceCreatePaymentParamsWithTimeout creates a new ArkServiceCreatePaymentParams object
// with the ability to set a timeout on a request.
func NewArkServiceCreatePaymentParamsWithTimeout(timeout time.Duration) *ArkServiceCreatePaymentParams {
return &ArkServiceCreatePaymentParams{
timeout: timeout,
}
}
// NewArkServiceCreatePaymentParamsWithContext creates a new ArkServiceCreatePaymentParams object
// with the ability to set a context for a request.
func NewArkServiceCreatePaymentParamsWithContext(ctx context.Context) *ArkServiceCreatePaymentParams {
return &ArkServiceCreatePaymentParams{
Context: ctx,
}
}
// NewArkServiceCreatePaymentParamsWithHTTPClient creates a new ArkServiceCreatePaymentParams object
// with the ability to set a custom HTTPClient for a request.
func NewArkServiceCreatePaymentParamsWithHTTPClient(client *http.Client) *ArkServiceCreatePaymentParams {
return &ArkServiceCreatePaymentParams{
HTTPClient: client,
}
}
/*
ArkServiceCreatePaymentParams contains all the parameters to send to the API endpoint
for the ark service create payment operation.
Typically these are written to a http.Request.
*/
type ArkServiceCreatePaymentParams struct {
// Body.
Body *models.V1CreatePaymentRequest
timeout time.Duration
Context context.Context
HTTPClient *http.Client
}
// WithDefaults hydrates default values in the ark service create payment params (not the query body).
//
// All values with no default are reset to their zero value.
func (o *ArkServiceCreatePaymentParams) WithDefaults() *ArkServiceCreatePaymentParams {
o.SetDefaults()
return o
}
// SetDefaults hydrates default values in the ark service create payment params (not the query body).
//
// All values with no default are reset to their zero value.
func (o *ArkServiceCreatePaymentParams) SetDefaults() {
// no default values defined for this parameter
}
// WithTimeout adds the timeout to the ark service create payment params
func (o *ArkServiceCreatePaymentParams) WithTimeout(timeout time.Duration) *ArkServiceCreatePaymentParams {
o.SetTimeout(timeout)
return o
}
// SetTimeout adds the timeout to the ark service create payment params
func (o *ArkServiceCreatePaymentParams) SetTimeout(timeout time.Duration) {
o.timeout = timeout
}
// WithContext adds the context to the ark service create payment params
func (o *ArkServiceCreatePaymentParams) WithContext(ctx context.Context) *ArkServiceCreatePaymentParams {
o.SetContext(ctx)
return o
}
// SetContext adds the context to the ark service create payment params
func (o *ArkServiceCreatePaymentParams) SetContext(ctx context.Context) {
o.Context = ctx
}
// WithHTTPClient adds the HTTPClient to the ark service create payment params
func (o *ArkServiceCreatePaymentParams) WithHTTPClient(client *http.Client) *ArkServiceCreatePaymentParams {
o.SetHTTPClient(client)
return o
}
// SetHTTPClient adds the HTTPClient to the ark service create payment params
func (o *ArkServiceCreatePaymentParams) SetHTTPClient(client *http.Client) {
o.HTTPClient = client
}
// WithBody adds the body to the ark service create payment params
func (o *ArkServiceCreatePaymentParams) WithBody(body *models.V1CreatePaymentRequest) *ArkServiceCreatePaymentParams {
o.SetBody(body)
return o
}
// SetBody adds the body to the ark service create payment params
func (o *ArkServiceCreatePaymentParams) SetBody(body *models.V1CreatePaymentRequest) {
o.Body = body
}
// WriteToRequest writes these params to a swagger request
func (o *ArkServiceCreatePaymentParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error {
if err := r.SetTimeout(o.timeout); err != nil {
return err
}
var res []error
if o.Body != nil {
if err := r.SetBodyParam(o.Body); err != nil {
return err
}
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}

View File

@@ -0,0 +1,187 @@
// Code generated by go-swagger; DO NOT EDIT.
package ark_service
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"encoding/json"
"fmt"
"io"
"github.com/go-openapi/runtime"
"github.com/go-openapi/strfmt"
"github.com/ark-network/ark/pkg/client-sdk/client/rest/service/models"
)
// ArkServiceCreatePaymentReader is a Reader for the ArkServiceCreatePayment structure.
type ArkServiceCreatePaymentReader struct {
formats strfmt.Registry
}
// ReadResponse reads a server response into the received o.
func (o *ArkServiceCreatePaymentReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
switch response.Code() {
case 200:
result := NewArkServiceCreatePaymentOK()
if err := result.readResponse(response, consumer, o.formats); err != nil {
return nil, err
}
return result, nil
default:
result := NewArkServiceCreatePaymentDefault(response.Code())
if err := result.readResponse(response, consumer, o.formats); err != nil {
return nil, err
}
if response.Code()/100 == 2 {
return result, nil
}
return nil, result
}
}
// NewArkServiceCreatePaymentOK creates a ArkServiceCreatePaymentOK with default headers values
func NewArkServiceCreatePaymentOK() *ArkServiceCreatePaymentOK {
return &ArkServiceCreatePaymentOK{}
}
/*
ArkServiceCreatePaymentOK describes a response with status code 200, with default header values.
A successful response.
*/
type ArkServiceCreatePaymentOK struct {
Payload *models.V1CreatePaymentResponse
}
// IsSuccess returns true when this ark service create payment o k response has a 2xx status code
func (o *ArkServiceCreatePaymentOK) IsSuccess() bool {
return true
}
// IsRedirect returns true when this ark service create payment o k response has a 3xx status code
func (o *ArkServiceCreatePaymentOK) IsRedirect() bool {
return false
}
// IsClientError returns true when this ark service create payment o k response has a 4xx status code
func (o *ArkServiceCreatePaymentOK) IsClientError() bool {
return false
}
// IsServerError returns true when this ark service create payment o k response has a 5xx status code
func (o *ArkServiceCreatePaymentOK) IsServerError() bool {
return false
}
// IsCode returns true when this ark service create payment o k response a status code equal to that given
func (o *ArkServiceCreatePaymentOK) IsCode(code int) bool {
return code == 200
}
// Code gets the status code for the ark service create payment o k response
func (o *ArkServiceCreatePaymentOK) Code() int {
return 200
}
func (o *ArkServiceCreatePaymentOK) Error() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[POST /v1/payment][%d] arkServiceCreatePaymentOK %s", 200, payload)
}
func (o *ArkServiceCreatePaymentOK) String() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[POST /v1/payment][%d] arkServiceCreatePaymentOK %s", 200, payload)
}
func (o *ArkServiceCreatePaymentOK) GetPayload() *models.V1CreatePaymentResponse {
return o.Payload
}
func (o *ArkServiceCreatePaymentOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
o.Payload = new(models.V1CreatePaymentResponse)
// response payload
if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
return err
}
return nil
}
// NewArkServiceCreatePaymentDefault creates a ArkServiceCreatePaymentDefault with default headers values
func NewArkServiceCreatePaymentDefault(code int) *ArkServiceCreatePaymentDefault {
return &ArkServiceCreatePaymentDefault{
_statusCode: code,
}
}
/*
ArkServiceCreatePaymentDefault describes a response with status code -1, with default header values.
An unexpected error response.
*/
type ArkServiceCreatePaymentDefault struct {
_statusCode int
Payload *models.RPCStatus
}
// IsSuccess returns true when this ark service create payment default response has a 2xx status code
func (o *ArkServiceCreatePaymentDefault) IsSuccess() bool {
return o._statusCode/100 == 2
}
// IsRedirect returns true when this ark service create payment default response has a 3xx status code
func (o *ArkServiceCreatePaymentDefault) IsRedirect() bool {
return o._statusCode/100 == 3
}
// IsClientError returns true when this ark service create payment default response has a 4xx status code
func (o *ArkServiceCreatePaymentDefault) IsClientError() bool {
return o._statusCode/100 == 4
}
// IsServerError returns true when this ark service create payment default response has a 5xx status code
func (o *ArkServiceCreatePaymentDefault) IsServerError() bool {
return o._statusCode/100 == 5
}
// IsCode returns true when this ark service create payment default response a status code equal to that given
func (o *ArkServiceCreatePaymentDefault) IsCode(code int) bool {
return o._statusCode == code
}
// Code gets the status code for the ark service create payment default response
func (o *ArkServiceCreatePaymentDefault) Code() int {
return o._statusCode
}
func (o *ArkServiceCreatePaymentDefault) Error() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[POST /v1/payment][%d] ArkService_CreatePayment default %s", o._statusCode, payload)
}
func (o *ArkServiceCreatePaymentDefault) String() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[POST /v1/payment][%d] ArkService_CreatePayment default %s", o._statusCode, payload)
}
func (o *ArkServiceCreatePaymentDefault) GetPayload() *models.RPCStatus {
return o.Payload
}
func (o *ArkServiceCreatePaymentDefault) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
o.Payload = new(models.RPCStatus)
// response payload
if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
return err
}
return nil
}

View File

@@ -0,0 +1,53 @@
// 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/strfmt"
"github.com/go-openapi/swag"
)
// V1CompletePaymentRequest v1 complete payment request
//
// swagger:model v1CompletePaymentRequest
type V1CompletePaymentRequest struct {
// signed redeem tx
SignedRedeemTx string `json:"signedRedeemTx,omitempty"`
// signed unconditional forfeit txs
SignedUnconditionalForfeitTxs []string `json:"signedUnconditionalForfeitTxs"`
}
// Validate validates this v1 complete payment request
func (m *V1CompletePaymentRequest) Validate(formats strfmt.Registry) error {
return nil
}
// ContextValidate validates this v1 complete payment request based on context it is used
func (m *V1CompletePaymentRequest) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *V1CompletePaymentRequest) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *V1CompletePaymentRequest) UnmarshalBinary(b []byte) error {
var res V1CompletePaymentRequest
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -0,0 +1,11 @@
// 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
// V1CompletePaymentResponse v1 complete payment response
//
// swagger:model v1CompletePaymentResponse
type V1CompletePaymentResponse interface{}

View File

@@ -0,0 +1,183 @@
// 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"
"strconv"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// V1CreatePaymentRequest v1 create payment request
//
// swagger:model v1CreatePaymentRequest
type V1CreatePaymentRequest struct {
// inputs
Inputs []*V1Input `json:"inputs"`
// outputs
Outputs []*V1Output `json:"outputs"`
}
// Validate validates this v1 create payment request
func (m *V1CreatePaymentRequest) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateInputs(formats); err != nil {
res = append(res, err)
}
if err := m.validateOutputs(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *V1CreatePaymentRequest) validateInputs(formats strfmt.Registry) error {
if swag.IsZero(m.Inputs) { // not required
return nil
}
for i := 0; i < len(m.Inputs); i++ {
if swag.IsZero(m.Inputs[i]) { // not required
continue
}
if m.Inputs[i] != nil {
if err := m.Inputs[i].Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("inputs" + "." + strconv.Itoa(i))
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("inputs" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}
func (m *V1CreatePaymentRequest) validateOutputs(formats strfmt.Registry) error {
if swag.IsZero(m.Outputs) { // not required
return nil
}
for i := 0; i < len(m.Outputs); i++ {
if swag.IsZero(m.Outputs[i]) { // not required
continue
}
if m.Outputs[i] != nil {
if err := m.Outputs[i].Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("outputs" + "." + strconv.Itoa(i))
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("outputs" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}
// ContextValidate validate this v1 create payment request based on the context it is used
func (m *V1CreatePaymentRequest) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
var res []error
if err := m.contextValidateInputs(ctx, formats); err != nil {
res = append(res, err)
}
if err := m.contextValidateOutputs(ctx, formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *V1CreatePaymentRequest) contextValidateInputs(ctx context.Context, formats strfmt.Registry) error {
for i := 0; i < len(m.Inputs); i++ {
if m.Inputs[i] != nil {
if swag.IsZero(m.Inputs[i]) { // not required
return nil
}
if err := m.Inputs[i].ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("inputs" + "." + strconv.Itoa(i))
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("inputs" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}
func (m *V1CreatePaymentRequest) contextValidateOutputs(ctx context.Context, formats strfmt.Registry) error {
for i := 0; i < len(m.Outputs); i++ {
if m.Outputs[i] != nil {
if swag.IsZero(m.Outputs[i]) { // not required
return nil
}
if err := m.Outputs[i].ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("outputs" + "." + strconv.Itoa(i))
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("outputs" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}
// MarshalBinary interface implementation
func (m *V1CreatePaymentRequest) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *V1CreatePaymentRequest) UnmarshalBinary(b []byte) error {
var res V1CreatePaymentRequest
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -0,0 +1,53 @@
// 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/strfmt"
"github.com/go-openapi/swag"
)
// V1CreatePaymentResponse v1 create payment response
//
// swagger:model v1CreatePaymentResponse
type V1CreatePaymentResponse struct {
// signed only by the ASP
SignedRedeemTx string `json:"signedRedeemTx,omitempty"`
// usigned unconditional forfeit txs
UsignedUnconditionalForfeitTxs []string `json:"usignedUnconditionalForfeitTxs"`
}
// Validate validates this v1 create payment response
func (m *V1CreatePaymentResponse) Validate(formats strfmt.Registry) error {
return nil
}
// ContextValidate validates this v1 create payment response based on context it is used
func (m *V1CreatePaymentResponse) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *V1CreatePaymentResponse) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *V1CreatePaymentResponse) UnmarshalBinary(b []byte) error {
var res V1CreatePaymentResponse
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -0,0 +1,53 @@
// 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/strfmt"
"github.com/go-openapi/swag"
)
// V1PendingPayment v1 pending payment
//
// swagger:model v1PendingPayment
type V1PendingPayment struct {
// redeem tx
RedeemTx string `json:"redeemTx,omitempty"`
// unconditional forfeit txs
UnconditionalForfeitTxs []string `json:"unconditionalForfeitTxs"`
}
// Validate validates this v1 pending payment
func (m *V1PendingPayment) Validate(formats strfmt.Registry) error {
return nil
}
// ContextValidate validates this v1 pending payment based on context it is used
func (m *V1PendingPayment) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *V1PendingPayment) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *V1PendingPayment) UnmarshalBinary(b []byte) error {
var res V1PendingPayment
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -24,6 +24,12 @@ type V1Vtxo struct {
// outpoint // outpoint
Outpoint *V1Input `json:"outpoint,omitempty"` Outpoint *V1Input `json:"outpoint,omitempty"`
// pending
Pending bool `json:"pending,omitempty"`
// pending data
PendingData *V1PendingPayment `json:"pendingData,omitempty"`
// pool txid // pool txid
PoolTxid string `json:"poolTxid,omitempty"` PoolTxid string `json:"poolTxid,omitempty"`
@@ -48,6 +54,10 @@ func (m *V1Vtxo) Validate(formats strfmt.Registry) error {
res = append(res, err) res = append(res, err)
} }
if err := m.validatePendingData(formats); err != nil {
res = append(res, err)
}
if err := m.validateReceiver(formats); err != nil { if err := m.validateReceiver(formats); err != nil {
res = append(res, err) res = append(res, err)
} }
@@ -77,6 +87,25 @@ func (m *V1Vtxo) validateOutpoint(formats strfmt.Registry) error {
return nil return nil
} }
func (m *V1Vtxo) validatePendingData(formats strfmt.Registry) error {
if swag.IsZero(m.PendingData) { // not required
return nil
}
if m.PendingData != nil {
if err := m.PendingData.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("pendingData")
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("pendingData")
}
return err
}
}
return nil
}
func (m *V1Vtxo) validateReceiver(formats strfmt.Registry) error { func (m *V1Vtxo) validateReceiver(formats strfmt.Registry) error {
if swag.IsZero(m.Receiver) { // not required if swag.IsZero(m.Receiver) { // not required
return nil return nil
@@ -104,6 +133,10 @@ func (m *V1Vtxo) ContextValidate(ctx context.Context, formats strfmt.Registry) e
res = append(res, err) res = append(res, err)
} }
if err := m.contextValidatePendingData(ctx, formats); err != nil {
res = append(res, err)
}
if err := m.contextValidateReceiver(ctx, formats); err != nil { if err := m.contextValidateReceiver(ctx, formats); err != nil {
res = append(res, err) res = append(res, err)
} }
@@ -135,6 +168,27 @@ func (m *V1Vtxo) contextValidateOutpoint(ctx context.Context, formats strfmt.Reg
return nil return nil
} }
func (m *V1Vtxo) contextValidatePendingData(ctx context.Context, formats strfmt.Registry) error {
if m.PendingData != nil {
if swag.IsZero(m.PendingData) { // not required
return nil
}
if err := m.PendingData.ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("pendingData")
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("pendingData")
}
return err
}
}
return nil
}
func (m *V1Vtxo) contextValidateReceiver(ctx context.Context, formats strfmt.Registry) error { func (m *V1Vtxo) contextValidateReceiver(ctx context.Context, formats strfmt.Registry) error {
if m.Receiver != nil { if m.Receiver != nil {

View File

@@ -547,6 +547,17 @@ func (a *covenantArkClient) CollaborativeRedeem(
return poolTxID, nil return poolTxID, nil
} }
func (a *covenantArkClient) SendAsync(
ctx context.Context,
withExpiryCoinselect bool, receivers []Receiver,
) (string, error) {
return "", fmt.Errorf("not implemented")
}
func (a *covenantArkClient) ClaimAsync(ctx context.Context) (string, error) {
return "", fmt.Errorf("not implemented")
}
func (a *covenantArkClient) sendOnchain( func (a *covenantArkClient) sendOnchain(
ctx context.Context, receivers []Receiver, ctx context.Context, receivers []Receiver,
) (string, error) { ) (string, error) {

View File

@@ -478,7 +478,7 @@ func (a *covenantlessArkClient) UnilateralRedeem(ctx context.Context) error {
vtxos := make([]client.Vtxo, 0) vtxos := make([]client.Vtxo, 0)
for _, offchainAddr := range offchainAddrs { for _, offchainAddr := range offchainAddrs {
spendableVtxos, err := a.getVtxos(ctx, offchainAddr, false) spendableVtxos, _, err := a.getVtxos(ctx, offchainAddr, false)
if err != nil { if err != nil {
return err return err
} }
@@ -574,7 +574,7 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
vtxos := make([]client.Vtxo, 0) vtxos := make([]client.Vtxo, 0)
for _, offchainAddr := range offchainAddrs { for _, offchainAddr := range offchainAddrs {
spendableVtxos, err := a.getVtxos(ctx, offchainAddr, withExpiryCoinselect) spendableVtxos, _, err := a.getVtxos(ctx, offchainAddr, withExpiryCoinselect)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -627,6 +627,145 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
return poolTxID, nil return poolTxID, nil
} }
func (a *covenantlessArkClient) SendAsync(
ctx context.Context,
withExpiryCoinselect bool, receivers []Receiver,
) (string, error) {
if len(receivers) <= 0 {
return "", fmt.Errorf("missing receivers")
}
for _, receiver := range receivers {
isOnchain, _, _, err := utils.DecodeReceiverAddress(receiver.To())
if err != nil {
return "", err
}
if isOnchain {
return "", fmt.Errorf("all receiver addresses must be offchain addresses")
}
}
offchainAddrs, _, _, err := a.wallet.GetAddresses(ctx)
if err != nil {
return "", err
}
_, _, aspPubKey, err := common.DecodeAddress(offchainAddrs[0])
if err != nil {
return "", err
}
receiversOutput := make([]client.Output, 0)
sumOfReceivers := uint64(0)
for _, receiver := range receivers {
_, _, aspKey, err := common.DecodeAddress(receiver.To())
if err != nil {
return "", fmt.Errorf("invalid receiver address: %s", err)
}
if !bytes.Equal(
aspPubKey.SerializeCompressed(), aspKey.SerializeCompressed(),
) {
return "", fmt.Errorf("invalid receiver address '%s': must be associated with the connected service provider", receiver)
}
if receiver.Amount() < DUST {
return "", fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount(), DUST)
}
receiversOutput = append(receiversOutput, client.Output{
Address: receiver.To(),
Amount: receiver.Amount(),
})
sumOfReceivers += receiver.Amount()
}
vtxos, _, err := a.getVtxos(ctx, offchainAddrs[0], withExpiryCoinselect)
if err != nil {
return "", err
}
selectedCoins, changeAmount, err := utils.CoinSelect(
vtxos, sumOfReceivers, DUST, withExpiryCoinselect,
)
if err != nil {
return "", err
}
if changeAmount > 0 {
changeReceiver := client.Output{
Address: offchainAddrs[0],
Amount: changeAmount,
}
receiversOutput = append(receiversOutput, changeReceiver)
}
inputs := make([]client.VtxoKey, 0, len(selectedCoins))
for _, coin := range selectedCoins {
inputs = append(inputs, coin.VtxoKey)
}
redeemTx, unconditionalForfeitTxs, err := a.client.CreatePayment(
ctx, inputs, receiversOutput)
if err != nil {
return "", err
}
// TODO verify the redeem tx signature
signedUnconditionalForfeitTxs := make([]string, 0, len(unconditionalForfeitTxs))
for _, tx := range unconditionalForfeitTxs {
signedForfeitTx, err := a.wallet.SignTransaction(ctx, a.explorer, tx)
if err != nil {
return "", err
}
signedUnconditionalForfeitTxs = append(signedUnconditionalForfeitTxs, signedForfeitTx)
}
signedRedeemTx, err := a.wallet.SignTransaction(ctx, a.explorer, redeemTx)
if err != nil {
return "", err
}
if err = a.client.CompletePayment(
ctx, signedRedeemTx, signedUnconditionalForfeitTxs,
); err != nil {
return "", err
}
return signedRedeemTx, nil
}
func (a *covenantlessArkClient) ClaimAsync(
ctx context.Context,
) (string, error) {
myselfOffchain, _, err := a.wallet.NewAddress(ctx, false)
if err != nil {
return "", err
}
_, pendingVtxos, err := a.getVtxos(ctx, myselfOffchain, false)
if err != nil {
return "", err
}
var pendingBalance uint64
for _, vtxo := range pendingVtxos {
pendingBalance += vtxo.Amount
}
if pendingBalance == 0 {
return "", nil
}
receiver := client.Output{
Address: myselfOffchain,
Amount: pendingBalance,
}
return a.selfTransferAllPendingPayments(ctx, pendingVtxos, receiver)
}
func (a *covenantlessArkClient) sendOnchain( func (a *covenantlessArkClient) sendOnchain(
ctx context.Context, receivers []Receiver, ctx context.Context, receivers []Receiver,
) (string, error) { ) (string, error) {
@@ -817,7 +956,7 @@ func (a *covenantlessArkClient) sendOffchain(
vtxos := make([]client.Vtxo, 0) vtxos := make([]client.Vtxo, 0)
for _, offchainAddr := range offchainAddrs { for _, offchainAddr := range offchainAddrs {
spendableVtxos, err := a.getVtxos(ctx, offchainAddr, withExpiryCoinselect) spendableVtxos, _, err := a.getVtxos(ctx, offchainAddr, withExpiryCoinselect)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -1394,16 +1533,20 @@ func (a *covenantlessArkClient) getRedeemBranches(
return redeemBranches, nil return redeemBranches, nil
} }
// TODO (@louisinger): return pending balance in dedicated map.
// Currently, the returned balance is calculated from both spendable and
// pending vtxos.
func (a *covenantlessArkClient) getOffchainBalance( func (a *covenantlessArkClient) getOffchainBalance(
ctx context.Context, addr string, computeVtxoExpiration bool, ctx context.Context, addr string, 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) spendableVtxos, pendingVtxos, err := a.getVtxos(ctx, addr, computeVtxoExpiration)
if err != nil { if err != nil {
return 0, nil, err return 0, nil, err
} }
var balance uint64 var balance uint64
vtxos := append(spendableVtxos, pendingVtxos...)
for _, vtxo := range vtxos { for _, vtxo := range vtxos {
balance += vtxo.Amount balance += vtxo.Amount
@@ -1423,28 +1566,38 @@ func (a *covenantlessArkClient) getOffchainBalance(
func (a *covenantlessArkClient) getVtxos( func (a *covenantlessArkClient) getVtxos(
ctx context.Context, addr string, computeVtxoExpiration bool, ctx context.Context, addr string, computeVtxoExpiration bool,
) ([]client.Vtxo, error) { ) ([]client.Vtxo, []client.Vtxo, error) {
vtxos, _, err := a.client.ListVtxos(ctx, addr) vtxos, _, err := a.client.ListVtxos(ctx, addr)
if err != nil { if err != nil {
return nil, err return nil, nil, err
}
pendingVtxos := make([]client.Vtxo, 0)
spendableVtxos := make([]client.Vtxo, 0)
for _, vtxo := range vtxos {
if vtxo.Pending {
pendingVtxos = append(pendingVtxos, vtxo)
continue
}
spendableVtxos = append(spendableVtxos, vtxo)
} }
if !computeVtxoExpiration { if !computeVtxoExpiration {
return vtxos, nil return spendableVtxos, pendingVtxos, nil
} }
redeemBranches, err := a.getRedeemBranches(ctx, vtxos) redeemBranches, err := a.getRedeemBranches(ctx, spendableVtxos)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
for vtxoTxid, branch := range redeemBranches { for vtxoTxid, branch := range redeemBranches {
expiration, err := branch.ExpiresAt() expiration, err := branch.ExpiresAt()
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
for i, vtxo := range vtxos { for i, vtxo := range spendableVtxos {
if vtxo.Txid == vtxoTxid { if vtxo.Txid == vtxoTxid {
vtxos[i].ExpiresAt = expiration vtxos[i].ExpiresAt = expiration
break break
@@ -1452,5 +1605,35 @@ func (a *covenantlessArkClient) getVtxos(
} }
} }
return vtxos, nil return spendableVtxos, pendingVtxos, nil
}
func (a *covenantlessArkClient) selfTransferAllPendingPayments(
ctx context.Context, pendingVtxos []client.Vtxo, myself client.Output,
) (string, error) {
inputs := make([]client.VtxoKey, 0, len(pendingVtxos))
for _, coin := range pendingVtxos {
inputs = append(inputs, coin.VtxoKey)
}
outputs := []client.Output{myself}
paymentID, err := a.client.RegisterPayment(ctx, inputs)
if err != nil {
return "", err
}
if err := a.client.ClaimPayment(ctx, paymentID, outputs); err != nil {
return "", err
}
roundTxid, err := a.handleRoundStream(
ctx, paymentID, pendingVtxos, outputs,
)
if err != nil {
return "", err
}
return roundTxid, nil
} }

View File

@@ -106,12 +106,11 @@ func main() {
fmt.Println("") fmt.Println("")
log.Infof("alice is sending %d sats to bob offchain...", amount) log.Infof("alice is sending %d sats to bob offchain...", amount)
txid, err = aliceArkClient.SendOffChain(ctx, false, receivers) if _, err = aliceArkClient.SendAsync(ctx, false, receivers); err != nil {
if err != nil {
log.Fatal(err) log.Fatal(err)
} }
log.Infof("payment completed in round tx: %s", txid) log.Info("payment completed out of round")
if err := generateBlock(); err != nil { if err := generateBlock(); err != nil {
log.Fatal(err) log.Fatal(err)
@@ -135,6 +134,15 @@ func main() {
log.Infof("bob onchain balance: %d", bobBalance.OnchainBalance.SpendableAmount) log.Infof("bob onchain balance: %d", bobBalance.OnchainBalance.SpendableAmount)
log.Infof("bob offchain balance: %d", bobBalance.OffchainBalance.Total) log.Infof("bob offchain balance: %d", bobBalance.OffchainBalance.Total)
fmt.Println("")
log.Info("bob is claiming the incoming payment...")
roundTxid, err := bobArkClient.ClaimAsync(ctx)
if err != nil {
log.Fatal(err)
}
log.Infof("bob claimed the incoming payment in round %s", roundTxid)
} }
func setupArkClient() (arksdk.ArkClient, error) { func setupArkClient() (arksdk.ArkClient, error) {

View File

@@ -15,7 +15,6 @@ import (
"github.com/ark-network/ark/pkg/client-sdk/internal/utils" "github.com/ark-network/ark/pkg/client-sdk/internal/utils"
"github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
log "github.com/sirupsen/logrus"
"github.com/vulpemventures/go-elements/psetv2" "github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/transaction" "github.com/vulpemventures/go-elements/transaction"
) )
@@ -97,8 +96,7 @@ func (e *explorerSvc) GetFeeRate() (float64, error) {
} }
if len(response) == 0 { if len(response) == 0 {
log.Debug("empty fee-estimates response, default to 2 sat/vbyte") return 1, nil
return 2, nil
} }
return response["1"], nil return response["1"], nil

View File

@@ -201,7 +201,7 @@ func (s *bitcoinWallet) SignTransaction(
switch c := closure.(type) { switch c := closure.(type) {
case *bitcointree.CSVSigClosure: case *bitcointree.CSVSigClosure:
sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], pubkey.SerializeCompressed()[1:]) sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], pubkey.SerializeCompressed()[1:])
case *bitcointree.ForfeitClosure: case *bitcointree.MultisigClosure:
sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], pubkey.SerializeCompressed()[1:]) sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], pubkey.SerializeCompressed()[1:])
} }

View File

@@ -33,13 +33,16 @@ lint:
## run: run in dev mode ## run: run in dev mode
run: clean run: clean
@echo "Running arkd in dev mode..." @echo "Running arkd in dev mode..."
@export ARK_WALLET_ADDR=localhost:18000; \ @export ARK_NEUTRINO_PEER=localhost:18444; \
export ARK_ROUND_INTERVAL=10; \ export ARK_ROUND_INTERVAL=10; \
export ARK_LOG_LEVEL=5; \ export ARK_LOG_LEVEL=5; \
export ARK_NETWORK=liquidregtest; \ export ARK_NETWORK=regtest; \
export ARK_PORT=8080; \ export ARK_PORT=8080; \
export ARK_NO_TLS=true; \ export ARK_NO_TLS=true; \
export ARK_NO_MACAROONS=true; \ export ARK_NO_MACAROONS=true; \
export ARK_TX_BUILDER_TYPE=covenantless; \
export ARK_ESPLORA_URL=http://localhost:3000; \
export ARK_MIN_RELAY_FEE=200; \
go run ./cmd/arkd go run ./cmd/arkd
## test: runs unit and component tests ## test: runs unit and component tests

View File

@@ -101,6 +101,38 @@
] ]
} }
}, },
"/v1/payment": {
"post": {
"operationId": "ArkService_CreatePayment",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1CreatePaymentResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1CreatePaymentRequest"
}
}
],
"tags": [
"ArkService"
]
}
},
"/v1/payment/claim": { "/v1/payment/claim": {
"post": { "post": {
"operationId": "ArkService_ClaimPayment", "operationId": "ArkService_ClaimPayment",
@@ -133,6 +165,38 @@
] ]
} }
}, },
"/v1/payment/complete": {
"post": {
"operationId": "ArkService_CompletePayment",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1CompletePaymentResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1CompletePaymentRequest"
}
}
],
"tags": [
"ArkService"
]
}
},
"/v1/payment/finalize": { "/v1/payment/finalize": {
"post": { "post": {
"operationId": "ArkService_FinalizePayment", "operationId": "ArkService_FinalizePayment",
@@ -368,6 +432,57 @@
"v1ClaimPaymentResponse": { "v1ClaimPaymentResponse": {
"type": "object" "type": "object"
}, },
"v1CompletePaymentRequest": {
"type": "object",
"properties": {
"signedRedeemTx": {
"type": "string"
},
"signedUnconditionalForfeitTxs": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"v1CompletePaymentResponse": {
"type": "object"
},
"v1CreatePaymentRequest": {
"type": "object",
"properties": {
"inputs": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/v1Input"
}
},
"outputs": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/v1Output"
}
}
}
},
"v1CreatePaymentResponse": {
"type": "object",
"properties": {
"signedRedeemTx": {
"type": "string",
"title": "signed only by the ASP"
},
"usignedUnconditionalForfeitTxs": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"v1FinalizePaymentRequest": { "v1FinalizePaymentRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -517,6 +632,20 @@
} }
} }
}, },
"v1PendingPayment": {
"type": "object",
"properties": {
"redeemTx": {
"type": "string"
},
"unconditionalForfeitTxs": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"v1PingResponse": { "v1PingResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -696,6 +825,12 @@
}, },
"swept": { "swept": {
"type": "boolean" "type": "boolean"
},
"pending": {
"type": "boolean"
},
"pendingData": {
"$ref": "#/definitions/v1PendingPayment"
} }
} }
} }

View File

@@ -0,0 +1,820 @@
{
"swagger": "2.0",
"info": {
"title": "clark/v1/service.proto",
"version": "version not set"
},
"tags": [
{
"name": "ArkService"
}
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/v1/events": {
"get": {
"operationId": "ArkService_GetEventStream",
"responses": {
"200": {
"description": "A successful response.(streaming responses)",
"schema": {
"type": "object",
"properties": {
"result": {
"$ref": "#/definitions/v1GetEventStreamResponse"
},
"error": {
"$ref": "#/definitions/rpcStatus"
}
},
"title": "Stream result of v1GetEventStreamResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"tags": [
"ArkService"
]
}
},
"/v1/info": {
"get": {
"operationId": "ArkService_GetInfo",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1GetInfoResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"tags": [
"ArkService"
]
}
},
"/v1/onboard": {
"post": {
"operationId": "ArkService_Onboard",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1OnboardResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1OnboardRequest"
}
}
],
"tags": [
"ArkService"
]
}
},
"/v1/payment/async": {
"post": {
"operationId": "ArkService_CreateAsyncPayment",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1CreateAsyncPaymentResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1CreateAsyncPaymentRequest"
}
}
],
"tags": [
"ArkService"
]
}
},
"/v1/payment/async/complete": {
"post": {
"operationId": "ArkService_CompleteAsyncPayment",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1CompleteAsyncPaymentResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1CompleteAsyncPaymentRequest"
}
}
],
"tags": [
"ArkService"
]
}
},
"/v1/payment/claim": {
"post": {
"operationId": "ArkService_ClaimPayment",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1ClaimPaymentResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1ClaimPaymentRequest"
}
}
],
"tags": [
"ArkService"
]
}
},
"/v1/payment/finalize": {
"post": {
"operationId": "ArkService_FinalizePayment",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1FinalizePaymentResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1FinalizePaymentRequest"
}
}
],
"tags": [
"ArkService"
]
}
},
"/v1/payment/register": {
"post": {
"operationId": "ArkService_RegisterPayment",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1RegisterPaymentResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1RegisterPaymentRequest"
}
}
],
"tags": [
"ArkService"
]
}
},
"/v1/ping/{paymentId}": {
"get": {
"operationId": "ArkService_Ping",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1PingResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "paymentId",
"in": "path",
"required": true,
"type": "string"
}
],
"tags": [
"ArkService"
]
}
},
"/v1/round/id/{id}": {
"get": {
"operationId": "ArkService_GetRoundById",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1GetRoundByIdResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"type": "string"
}
],
"tags": [
"ArkService"
]
}
},
"/v1/round/{txid}": {
"get": {
"summary": "TODO BTC: signTree rpc",
"operationId": "ArkService_GetRound",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1GetRoundResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "txid",
"in": "path",
"required": true,
"type": "string"
}
],
"tags": [
"ArkService"
]
}
},
"/v1/vtxos/{address}": {
"get": {
"operationId": "ArkService_ListVtxos",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1ListVtxosResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "address",
"in": "path",
"required": true,
"type": "string"
}
],
"tags": [
"ArkService"
]
}
}
},
"definitions": {
"protobufAny": {
"type": "object",
"properties": {
"@type": {
"type": "string"
}
},
"additionalProperties": {}
},
"rpcStatus": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"details": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/protobufAny"
}
}
}
},
"v1ClaimPaymentRequest": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Mocks wabisabi's credentials."
},
"outputs": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/v1Output"
},
"description": "List of receivers for a registered payment."
}
}
},
"v1ClaimPaymentResponse": {
"type": "object"
},
"v1CompleteAsyncPaymentRequest": {
"type": "object",
"properties": {
"signedSenderTx": {
"type": "string"
},
"signedReceiverTx": {
"type": "string"
},
"signedUnconditionalForfeitTx": {
"type": "string"
}
}
},
"v1CompleteAsyncPaymentResponse": {
"type": "object",
"properties": {
"senderTx": {
"type": "string"
},
"receiverTx": {
"type": "string"
},
"unconditionalForfeitTx": {
"type": "string"
}
}
},
"v1CreateAsyncPaymentRequest": {
"type": "object",
"properties": {
"input": {
"$ref": "#/definitions/v1Input"
},
"receiverPubkey": {
"type": "string"
}
}
},
"v1CreateAsyncPaymentResponse": {
"type": "object",
"properties": {
"unsignedSenderTx": {
"type": "string"
},
"unsignedReceiverTx": {
"type": "string"
},
"usignedUnconditionalForfeitTx": {
"type": "string"
}
}
},
"v1FinalizePaymentRequest": {
"type": "object",
"properties": {
"signedForfeitTxs": {
"type": "array",
"items": {
"type": "string"
},
"description": "Forfeit txs signed by the user."
}
}
},
"v1FinalizePaymentResponse": {
"type": "object"
},
"v1GetEventStreamResponse": {
"type": "object",
"properties": {
"roundFinalization": {
"$ref": "#/definitions/v1RoundFinalizationEvent",
"title": "TODO: BTC add \"signTree\" event"
},
"roundFinalized": {
"$ref": "#/definitions/v1RoundFinalizedEvent"
},
"roundFailed": {
"$ref": "#/definitions/v1RoundFailed"
}
}
},
"v1GetInfoResponse": {
"type": "object",
"properties": {
"pubkey": {
"type": "string"
},
"roundLifetime": {
"type": "string",
"format": "int64"
},
"unilateralExitDelay": {
"type": "string",
"format": "int64"
},
"roundInterval": {
"type": "string",
"format": "int64"
},
"network": {
"type": "string"
},
"minRelayFee": {
"type": "string",
"format": "int64"
}
}
},
"v1GetRoundByIdResponse": {
"type": "object",
"properties": {
"round": {
"$ref": "#/definitions/v1Round"
}
}
},
"v1GetRoundResponse": {
"type": "object",
"properties": {
"round": {
"$ref": "#/definitions/v1Round"
}
}
},
"v1Input": {
"type": "object",
"properties": {
"txid": {
"type": "string"
},
"vout": {
"type": "integer",
"format": "int64"
}
}
},
"v1ListVtxosResponse": {
"type": "object",
"properties": {
"spendableVtxos": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/v1Vtxo"
}
},
"spentVtxos": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/v1Vtxo"
}
}
}
},
"v1Node": {
"type": "object",
"properties": {
"txid": {
"type": "string"
},
"tx": {
"type": "string"
},
"parentTxid": {
"type": "string"
}
}
},
"v1OnboardRequest": {
"type": "object",
"properties": {
"boardingTx": {
"type": "string"
},
"congestionTree": {
"$ref": "#/definitions/v1Tree"
},
"userPubkey": {
"type": "string"
}
}
},
"v1OnboardResponse": {
"type": "object"
},
"v1Output": {
"type": "object",
"properties": {
"address": {
"type": "string",
"description": "Either the offchain or onchain address."
},
"amount": {
"type": "string",
"format": "uint64",
"description": "Amount to send in satoshis."
}
}
},
"v1PingResponse": {
"type": "object",
"properties": {
"forfeitTxs": {
"type": "array",
"items": {
"type": "string"
}
},
"event": {
"$ref": "#/definitions/v1RoundFinalizationEvent"
}
}
},
"v1RegisterPaymentRequest": {
"type": "object",
"properties": {
"inputs": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/v1Input"
}
}
}
},
"v1RegisterPaymentResponse": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Mocks wabisabi's credentials."
}
}
},
"v1Round": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"start": {
"type": "string",
"format": "int64"
},
"end": {
"type": "string",
"format": "int64"
},
"poolTx": {
"type": "string"
},
"congestionTree": {
"$ref": "#/definitions/v1Tree"
},
"forfeitTxs": {
"type": "array",
"items": {
"type": "string"
}
},
"connectors": {
"type": "array",
"items": {
"type": "string"
}
},
"stage": {
"$ref": "#/definitions/v1RoundStage"
}
}
},
"v1RoundFailed": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"reason": {
"type": "string"
}
}
},
"v1RoundFinalizationEvent": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"poolTx": {
"type": "string"
},
"forfeitTxs": {
"type": "array",
"items": {
"type": "string"
}
},
"congestionTree": {
"$ref": "#/definitions/v1Tree"
},
"connectors": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"v1RoundFinalizedEvent": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"poolTxid": {
"type": "string"
}
}
},
"v1RoundStage": {
"type": "string",
"enum": [
"ROUND_STAGE_UNSPECIFIED",
"ROUND_STAGE_REGISTRATION",
"ROUND_STAGE_FINALIZATION",
"ROUND_STAGE_FINALIZED",
"ROUND_STAGE_FAILED"
],
"default": "ROUND_STAGE_UNSPECIFIED"
},
"v1Tree": {
"type": "object",
"properties": {
"levels": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/v1TreeLevel"
}
}
}
},
"v1TreeLevel": {
"type": "object",
"properties": {
"nodes": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/v1Node"
}
}
}
},
"v1Vtxo": {
"type": "object",
"properties": {
"outpoint": {
"$ref": "#/definitions/v1Input"
},
"receiver": {
"$ref": "#/definitions/v1Output"
},
"spent": {
"type": "boolean"
},
"poolTxid": {
"type": "string"
},
"spentBy": {
"type": "string"
},
"expireAt": {
"type": "string",
"format": "int64"
},
"swept": {
"type": "boolean"
}
}
}
}
}

View File

@@ -60,8 +60,35 @@ service ArkService {
body: "*" body: "*"
}; };
} }
rpc CreatePayment(CreatePaymentRequest) returns (CreatePaymentResponse) {
option (google.api.http) = {
post: "/v1/payment"
body: "*"
};
}
rpc CompletePayment(CompletePaymentRequest) returns (CompletePaymentResponse) {
option (google.api.http) = {
post: "/v1/payment/complete"
body: "*"
};
}
} }
message CreatePaymentRequest {
repeated Input inputs = 1;
repeated Output outputs = 2;
}
message CreatePaymentResponse {
string signed_redeem_tx = 1; // signed only by the ASP
repeated string usigned_unconditional_forfeit_txs = 2;
}
message CompletePaymentRequest {
string signed_redeem_tx = 1;
repeated string signed_unconditional_forfeit_txs = 2;
}
message CompletePaymentResponse {}
message RegisterPaymentRequest { message RegisterPaymentRequest {
repeated Input inputs = 1; repeated Input inputs = 1;
} }
@@ -217,4 +244,11 @@ message Vtxo {
string spent_by = 5; string spent_by = 5;
int64 expire_at = 6; int64 expire_at = 6;
bool swept = 7; bool swept = 7;
bool pending = 8;
PendingPayment pending_data = 9;
}
message PendingPayment {
string redeem_tx = 1;
repeated string unconditional_forfeit_txs =2;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -378,6 +378,58 @@ func local_request_ArkService_Onboard_0(ctx context.Context, marshaler runtime.M
} }
func request_ArkService_CreatePayment_0(ctx context.Context, marshaler runtime.Marshaler, client ArkServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq CreatePaymentRequest
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.CreatePayment(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_ArkService_CreatePayment_0(ctx context.Context, marshaler runtime.Marshaler, server ArkServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq CreatePaymentRequest
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.CreatePayment(ctx, &protoReq)
return msg, metadata, err
}
func request_ArkService_CompletePayment_0(ctx context.Context, marshaler runtime.Marshaler, client ArkServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq CompletePaymentRequest
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.CompletePayment(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_ArkService_CompletePayment_0(ctx context.Context, marshaler runtime.Marshaler, server ArkServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq CompletePaymentRequest
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.CompletePayment(ctx, &protoReq)
return msg, metadata, err
}
// RegisterArkServiceHandlerServer registers the http handlers for service ArkService to "mux". // RegisterArkServiceHandlerServer registers the http handlers for service ArkService to "mux".
// UnaryRPC :call ArkServiceServer directly. // UnaryRPC :call ArkServiceServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
@@ -617,6 +669,56 @@ func RegisterArkServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux,
}) })
mux.Handle("POST", pattern_ArkService_CreatePayment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/ark.v1.ArkService/CreatePayment", runtime.WithHTTPPathPattern("/v1/payment"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_ArkService_CreatePayment_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_ArkService_CreatePayment_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_ArkService_CompletePayment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/ark.v1.ArkService/CompletePayment", runtime.WithHTTPPathPattern("/v1/payment/complete"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_ArkService_CompletePayment_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_ArkService_CompletePayment_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil return nil
} }
@@ -878,6 +980,50 @@ func RegisterArkServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux,
}) })
mux.Handle("POST", pattern_ArkService_CreatePayment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/ark.v1.ArkService/CreatePayment", runtime.WithHTTPPathPattern("/v1/payment"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_ArkService_CreatePayment_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_ArkService_CreatePayment_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_ArkService_CompletePayment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/ark.v1.ArkService/CompletePayment", runtime.WithHTTPPathPattern("/v1/payment/complete"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_ArkService_CompletePayment_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_ArkService_CompletePayment_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil return nil
} }
@@ -901,6 +1047,10 @@ var (
pattern_ArkService_GetInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "info"}, "")) pattern_ArkService_GetInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "info"}, ""))
pattern_ArkService_Onboard_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "onboard"}, "")) pattern_ArkService_Onboard_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "onboard"}, ""))
pattern_ArkService_CreatePayment_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "payment"}, ""))
pattern_ArkService_CompletePayment_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "payment", "complete"}, ""))
) )
var ( var (
@@ -923,4 +1073,8 @@ var (
forward_ArkService_GetInfo_0 = runtime.ForwardResponseMessage forward_ArkService_GetInfo_0 = runtime.ForwardResponseMessage
forward_ArkService_Onboard_0 = runtime.ForwardResponseMessage forward_ArkService_Onboard_0 = runtime.ForwardResponseMessage
forward_ArkService_CreatePayment_0 = runtime.ForwardResponseMessage
forward_ArkService_CompletePayment_0 = runtime.ForwardResponseMessage
) )

View File

@@ -29,6 +29,8 @@ type ArkServiceClient interface {
ListVtxos(ctx context.Context, in *ListVtxosRequest, opts ...grpc.CallOption) (*ListVtxosResponse, error) ListVtxos(ctx context.Context, in *ListVtxosRequest, opts ...grpc.CallOption) (*ListVtxosResponse, error)
GetInfo(ctx context.Context, in *GetInfoRequest, opts ...grpc.CallOption) (*GetInfoResponse, error) GetInfo(ctx context.Context, in *GetInfoRequest, opts ...grpc.CallOption) (*GetInfoResponse, error)
Onboard(ctx context.Context, in *OnboardRequest, opts ...grpc.CallOption) (*OnboardResponse, error) Onboard(ctx context.Context, in *OnboardRequest, opts ...grpc.CallOption) (*OnboardResponse, error)
CreatePayment(ctx context.Context, in *CreatePaymentRequest, opts ...grpc.CallOption) (*CreatePaymentResponse, error)
CompletePayment(ctx context.Context, in *CompletePaymentRequest, opts ...grpc.CallOption) (*CompletePaymentResponse, error)
} }
type arkServiceClient struct { type arkServiceClient struct {
@@ -152,6 +154,24 @@ func (c *arkServiceClient) Onboard(ctx context.Context, in *OnboardRequest, opts
return out, nil return out, nil
} }
func (c *arkServiceClient) CreatePayment(ctx context.Context, in *CreatePaymentRequest, opts ...grpc.CallOption) (*CreatePaymentResponse, error) {
out := new(CreatePaymentResponse)
err := c.cc.Invoke(ctx, "/ark.v1.ArkService/CreatePayment", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *arkServiceClient) CompletePayment(ctx context.Context, in *CompletePaymentRequest, opts ...grpc.CallOption) (*CompletePaymentResponse, error) {
out := new(CompletePaymentResponse)
err := c.cc.Invoke(ctx, "/ark.v1.ArkService/CompletePayment", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// ArkServiceServer is the server API for ArkService service. // ArkServiceServer is the server API for ArkService service.
// All implementations should embed UnimplementedArkServiceServer // All implementations should embed UnimplementedArkServiceServer
// for forward compatibility // for forward compatibility
@@ -167,6 +187,8 @@ type ArkServiceServer interface {
ListVtxos(context.Context, *ListVtxosRequest) (*ListVtxosResponse, error) ListVtxos(context.Context, *ListVtxosRequest) (*ListVtxosResponse, error)
GetInfo(context.Context, *GetInfoRequest) (*GetInfoResponse, error) GetInfo(context.Context, *GetInfoRequest) (*GetInfoResponse, error)
Onboard(context.Context, *OnboardRequest) (*OnboardResponse, error) Onboard(context.Context, *OnboardRequest) (*OnboardResponse, error)
CreatePayment(context.Context, *CreatePaymentRequest) (*CreatePaymentResponse, error)
CompletePayment(context.Context, *CompletePaymentRequest) (*CompletePaymentResponse, error)
} }
// UnimplementedArkServiceServer should be embedded to have forward compatible implementations. // UnimplementedArkServiceServer should be embedded to have forward compatible implementations.
@@ -203,6 +225,12 @@ func (UnimplementedArkServiceServer) GetInfo(context.Context, *GetInfoRequest) (
func (UnimplementedArkServiceServer) Onboard(context.Context, *OnboardRequest) (*OnboardResponse, error) { func (UnimplementedArkServiceServer) Onboard(context.Context, *OnboardRequest) (*OnboardResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Onboard not implemented") return nil, status.Errorf(codes.Unimplemented, "method Onboard not implemented")
} }
func (UnimplementedArkServiceServer) CreatePayment(context.Context, *CreatePaymentRequest) (*CreatePaymentResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreatePayment not implemented")
}
func (UnimplementedArkServiceServer) CompletePayment(context.Context, *CompletePaymentRequest) (*CompletePaymentResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CompletePayment not implemented")
}
// UnsafeArkServiceServer may be embedded to opt out of forward compatibility for this service. // UnsafeArkServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to ArkServiceServer will // Use of this interface is not recommended, as added methods to ArkServiceServer will
@@ -398,6 +426,42 @@ func _ArkService_Onboard_Handler(srv interface{}, ctx context.Context, dec func(
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _ArkService_CreatePayment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreatePaymentRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ArkServiceServer).CreatePayment(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/ark.v1.ArkService/CreatePayment",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ArkServiceServer).CreatePayment(ctx, req.(*CreatePaymentRequest))
}
return interceptor(ctx, in, info, handler)
}
func _ArkService_CompletePayment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CompletePaymentRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ArkServiceServer).CompletePayment(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/ark.v1.ArkService/CompletePayment",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ArkServiceServer).CompletePayment(ctx, req.(*CompletePaymentRequest))
}
return interceptor(ctx, in, info, handler)
}
// ArkService_ServiceDesc is the grpc.ServiceDesc for ArkService service. // ArkService_ServiceDesc is the grpc.ServiceDesc for ArkService service.
// It's only intended for direct use with grpc.RegisterService, // It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy) // and not to be introspected or modified (even as a copy)
@@ -441,6 +505,14 @@ var ArkService_ServiceDesc = grpc.ServiceDesc{
MethodName: "Onboard", MethodName: "Onboard",
Handler: _ArkService_Onboard_Handler, Handler: _ArkService_Onboard_Handler,
}, },
{
MethodName: "CreatePayment",
Handler: _ArkService_CreatePayment_Handler,
},
{
MethodName: "CompletePayment",
Handler: _ArkService_CompletePayment_Handler,
},
}, },
Streams: []grpc.StreamDesc{ Streams: []grpc.StreamDesc{
{ {

View File

@@ -220,7 +220,7 @@ func post[T any](url, body, key, macaroon, tlsCert string) (result T, err error)
req.Header.Add("X-Macaroon", macaroon) req.Header.Add("X-Macaroon", macaroon)
} }
client := &http.Client{ client := &http.Client{
Timeout: 15 * time.Second, Timeout: 30 * time.Second,
Transport: &http.Transport{ Transport: &http.Transport{
TLSClientConfig: tlsConfig, TLSClientConfig: tlsConfig,
}, },
@@ -267,7 +267,7 @@ func get[T any](url, key, macaroon, tlsCert string) (result T, err error) {
} }
client := &http.Client{ client := &http.Client{
Timeout: 15 * time.Second, Timeout: 30 * time.Second,
Transport: &http.Transport{ Transport: &http.Transport{
TLSClientConfig: tlsConfig, TLSClientConfig: tlsConfig,
}, },
@@ -331,7 +331,7 @@ func getBalance(url, macaroon, tlsCert string) (*balance, error) {
req.Header.Add("X-Macaroon", macaroon) req.Header.Add("X-Macaroon", macaroon)
} }
client := &http.Client{ client := &http.Client{
Timeout: 15 * time.Second, Timeout: 30 * time.Second,
Transport: &http.Transport{ Transport: &http.Transport{
TLSClientConfig: tlsConfig, TLSClientConfig: tlsConfig,
}, },
@@ -384,7 +384,7 @@ func getStatus(url, tlsCert string) (*status, error) {
req.Header.Add("Content-Type", "application/json") req.Header.Add("Content-Type", "application/json")
client := &http.Client{ client := &http.Client{
Timeout: 15 * time.Second, Timeout: 30 * time.Second,
Transport: &http.Transport{ Transport: &http.Transport{
TLSClientConfig: tlsConfig, TLSClientConfig: tlsConfig,
}, },

View File

@@ -45,6 +45,8 @@ type AdminService interface {
GetScheduledSweeps(ctx context.Context) ([]ScheduledSweep, error) GetScheduledSweeps(ctx context.Context) ([]ScheduledSweep, error)
GetRoundDetails(ctx context.Context, roundId string) (*RoundDetails, error) GetRoundDetails(ctx context.Context, roundId string) (*RoundDetails, error)
GetRounds(ctx context.Context, after int64, before int64) ([]string, error) GetRounds(ctx context.Context, after int64, before int64) ([]string, error)
GetWalletAddress(ctx context.Context) (string, error)
GetWalletStatus(ctx context.Context) (*WalletStatus, error)
} }
type adminService struct { type adminService struct {
@@ -154,3 +156,24 @@ func (a *adminService) GetScheduledSweeps(ctx context.Context) ([]ScheduledSweep
return scheduledSweeps, nil return scheduledSweeps, nil
} }
func (a *adminService) GetWalletAddress(ctx context.Context) (string, error) {
addresses, err := a.walletSvc.DeriveAddresses(ctx, 1)
if err != nil {
return "", err
}
return addresses[0], nil
}
func (a *adminService) GetWalletStatus(ctx context.Context) (*WalletStatus, error) {
status, err := a.walletSvc.Status(ctx)
if err != nil {
return nil, err
}
return &WalletStatus{
IsInitialized: status.IsInitialized(),
IsUnlocked: status.IsUnlocked(),
IsSynced: status.IsSynced(),
}, nil
}

View File

@@ -161,6 +161,14 @@ func (s *covenantService) UpdatePaymentStatus(_ context.Context, id string) ([]s
return nil, nil, nil return nil, nil, nil
} }
func (s *covenantService) CompleteAsyncPayment(ctx context.Context, redeemTx string, unconditionalForfeitTxs []string) error {
return fmt.Errorf("unimplemented")
}
func (s *covenantService) CreateAsyncPayment(ctx context.Context, inputs []domain.VtxoKey, receivers []domain.Receiver) (string, []string, error) {
return "", nil, fmt.Errorf("unimplemented")
}
func (s *covenantService) SignVtxos(ctx context.Context, forfeitTxs []string) error { func (s *covenantService) SignVtxos(ctx context.Context, forfeitTxs []string) error {
return s.forfeitTxs.sign(forfeitTxs) return s.forfeitTxs.sign(forfeitTxs)
} }

View File

@@ -42,6 +42,11 @@ type covenantlessService struct {
onboardingCh chan onboarding onboardingCh chan onboarding
currentRound *domain.Round currentRound *domain.Round
asyncPaymentsCache map[domain.VtxoKey]struct {
receivers []domain.Receiver
expireAt int64
}
} }
func NewCovenantlessService( func NewCovenantlessService(
@@ -62,13 +67,30 @@ func NewCovenantlessService(
} }
sweeper := newSweeper(walletSvc, repoManager, builder, scheduler) sweeper := newSweeper(walletSvc, repoManager, builder, scheduler)
asyncPaymentsCache := make(map[domain.VtxoKey]struct {
receivers []domain.Receiver
expireAt int64
})
svc := &covenantlessService{ svc := &covenantlessService{
network, pubkey, network: network,
roundLifetime, roundInterval, unilateralExitDelay, minRelayFee, pubkey: pubkey,
walletSvc, repoManager, builder, scanner, sweeper, roundLifetime: roundLifetime,
paymentRequests, forfeitTxs, eventsCh, onboardingCh, nil, roundInterval: roundInterval,
unilateralExitDelay: unilateralExitDelay,
minRelayFee: minRelayFee,
wallet: walletSvc,
repoManager: repoManager,
builder: builder,
scanner: scanner,
sweeper: sweeper,
paymentRequests: paymentRequests,
forfeitTxs: forfeitTxs,
eventsCh: eventsCh,
onboardingCh: onboardingCh,
asyncPaymentsCache: asyncPaymentsCache,
} }
repoManager.RegisterEventsHandler( repoManager.RegisterEventsHandler(
func(round *domain.Round) { func(round *domain.Round) {
go svc.propagateEvents(round) go svc.propagateEvents(round)
@@ -115,6 +137,108 @@ func (s *covenantlessService) Stop() {
close(s.onboardingCh) close(s.onboardingCh)
} }
func (s *covenantlessService) CompleteAsyncPayment(
ctx context.Context, redeemTx string, unconditionalForfeitTxs []string,
) error {
// TODO check that the user signed both transactions
redeemPtx, err := psbt.NewFromRawBytes(strings.NewReader(redeemTx), true)
if err != nil {
return fmt.Errorf("failed to parse redeem tx: %s", err)
}
redeemTxid := redeemPtx.UnsignedTx.TxID()
spentVtxos := make([]domain.VtxoKey, 0, len(unconditionalForfeitTxs))
for _, in := range redeemPtx.UnsignedTx.TxIn {
spentVtxos = append(spentVtxos, domain.VtxoKey{
Txid: in.PreviousOutPoint.Hash.String(),
VOut: in.PreviousOutPoint.Index,
})
}
asyncPayData, ok := s.asyncPaymentsCache[spentVtxos[0]]
if !ok {
return fmt.Errorf("async payment not found")
}
vtxos := make([]domain.Vtxo, 0, len(asyncPayData.receivers))
for i, receiver := range asyncPayData.receivers {
vtxos = append(vtxos, domain.Vtxo{
VtxoKey: domain.VtxoKey{
Txid: redeemTxid,
VOut: uint32(i),
},
Receiver: receiver,
ExpireAt: asyncPayData.expireAt,
AsyncPayment: &domain.AsyncPaymentTxs{
RedeemTx: redeemTx,
UnconditionalForfeitTxs: unconditionalForfeitTxs,
},
})
}
if err := s.repoManager.Vtxos().AddVtxos(ctx, vtxos); err != nil {
return fmt.Errorf("failed to add vtxos: %s", err)
}
log.Infof("added %d vtxos", len(vtxos))
if err := s.repoManager.Vtxos().SpendVtxos(ctx, spentVtxos, redeemTxid); err != nil {
return fmt.Errorf("failed to spend vtxo: %s", err)
}
log.Infof("spent %d vtxos", len(spentVtxos))
delete(s.asyncPaymentsCache, spentVtxos[0])
return nil
}
func (s *covenantlessService) CreateAsyncPayment(
ctx context.Context, inputs []domain.VtxoKey, receivers []domain.Receiver,
) (string, []string, error) {
vtxos, err := s.repoManager.Vtxos().GetVtxos(ctx, inputs)
if err != nil {
return "", nil, err
}
if len(vtxos) <= 0 {
return "", nil, fmt.Errorf("vtxos not found")
}
expiration := vtxos[0].ExpireAt
for _, vtxo := range vtxos {
if vtxo.Spent {
return "", nil, fmt.Errorf("all vtxos must be unspent")
}
if vtxo.Redeemed {
return "", nil, fmt.Errorf("all vtxos must be redeemed")
}
if vtxo.Swept {
return "", nil, fmt.Errorf("all vtxos must be swept")
}
if vtxo.ExpireAt < expiration {
expiration = vtxo.ExpireAt
}
}
res, err := s.builder.BuildAsyncPaymentTransactions(
vtxos, s.pubkey, receivers, s.minRelayFee,
)
if err != nil {
return "", nil, fmt.Errorf("failed to build async payment txs: %s", err)
}
s.asyncPaymentsCache[inputs[0]] = struct {
receivers []domain.Receiver
expireAt int64
}{
receivers: receivers,
expireAt: expiration,
}
return res.RedeemTx, res.UnconditionalForfeitTxs, nil
}
func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []domain.VtxoKey) (string, error) { func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []domain.VtxoKey) (string, error) {
vtxos, err := s.repoManager.Vtxos().GetVtxos(ctx, inputs) vtxos, err := s.repoManager.Vtxos().GetVtxos(ctx, inputs)
if err != nil { if err != nil {

View File

@@ -149,6 +149,7 @@ func (s *sweeper) createTask(
for _, input := range inputs { for _, input := range inputs {
// sweepableVtxos related to the sweep input // sweepableVtxos related to the sweep input
sweepableVtxos := make([]domain.VtxoKey, 0) sweepableVtxos := make([]domain.VtxoKey, 0)
fmt.Println("input", input.GetHash().String(), input.GetIndex())
// check if input is the vtxo itself // check if input is the vtxo itself
vtxos, _ := s.repoManager.Vtxos().GetVtxos( vtxos, _ := s.repoManager.Vtxos().GetVtxos(

View File

@@ -34,6 +34,13 @@ type Service interface {
ctx context.Context, boardingTx string, ctx context.Context, boardingTx string,
congestionTree tree.CongestionTree, userPubkey *secp256k1.PublicKey, congestionTree tree.CongestionTree, userPubkey *secp256k1.PublicKey,
) error ) error
// Async payments
CreateAsyncPayment(
ctx context.Context, inputs []domain.VtxoKey, receivers []domain.Receiver,
) (string, []string, error)
CompleteAsyncPayment(
ctx context.Context, redeemTx string, unconditionalForfeitTxs []string,
) error
} }
type ServiceInfo struct { type ServiceInfo struct {

View File

@@ -130,10 +130,16 @@ func (r Receiver) IsOnchain() bool {
type Vtxo struct { type Vtxo struct {
VtxoKey VtxoKey
Receiver Receiver
PoolTx string PoolTx string
SpentBy string SpentBy string // round txid or async redeem txid
Spent bool Spent bool
Redeemed bool Redeemed bool
Swept bool Swept bool
ExpireAt int64 ExpireAt int64
AsyncPayment *AsyncPaymentTxs // nil if not async vtxo
}
type AsyncPaymentTxs struct {
RedeemTx string // always signed by the ASP when created
UnconditionalForfeitTxs []string
} }

View File

@@ -29,4 +29,8 @@ type TxBuilder interface {
FinalizeAndExtractForfeit(tx string) (txhex string, err error) FinalizeAndExtractForfeit(tx string) (txhex string, err error)
// FindLeaves returns all the leaves txs that are reachable from the given outpoint // FindLeaves returns all the leaves txs that are reachable from the given outpoint
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(
vtxosToSpend []domain.Vtxo,
aspPubKey *secp256k1.PublicKey, receivers []domain.Receiver, minRelayFee uint64,
) (*domain.AsyncPaymentTxs, error)
} }

View File

@@ -28,7 +28,7 @@ type WalletService interface {
SelectUtxos(ctx context.Context, asset string, amount uint64) ([]TxInput, uint64, error) SelectUtxos(ctx context.Context, asset string, amount uint64) ([]TxInput, uint64, error)
BroadcastTransaction(ctx context.Context, txHex string) (string, error) BroadcastTransaction(ctx context.Context, txHex string) (string, error)
WaitForSync(ctx context.Context, txid string) error WaitForSync(ctx context.Context, txid string) error
EstimateFees(ctx context.Context, pset string) (uint64, error) EstimateFees(ctx context.Context, psbt string) (uint64, error)
ListConnectorUtxos(ctx context.Context, connectorAddress string) ([]TxInput, error) ListConnectorUtxos(ctx context.Context, connectorAddress string) ([]TxInput, error)
MainAccountBalance(ctx context.Context) (uint64, uint64, error) MainAccountBalance(ctx context.Context) (uint64, uint64, error)
ConnectorsAccountBalance(ctx context.Context) (uint64, uint64, error) ConnectorsAccountBalance(ctx context.Context) (uint64, uint64, error)

View File

@@ -41,8 +41,17 @@ CREATE TABLE IF NOT EXISTS tx (
FOREIGN KEY (round_id) REFERENCES round(id) FOREIGN KEY (round_id) REFERENCES round(id)
); );
CREATE TABLE IF NOT EXISTS uncond_forfeit_tx (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tx TEXT NOT NULL,
vtxo_txid TEXT NOT NULL,
vtxo_vout INTEGER NOT NULL,
position INTEGER NOT NULL,
FOREIGN KEY (vtxo_txid, vtxo_vout) REFERENCES vtxo(txid, vout)
);
CREATE TABLE IF NOT EXISTS vtxo ( CREATE TABLE IF NOT EXISTS vtxo (
txid TEXT NOT NULL PRIMARY KEY, txid TEXT NOT NULL,
vout INTEGER NOT NULL, vout INTEGER NOT NULL,
pubkey TEXT NOT NULL, pubkey TEXT NOT NULL,
amount INTEGER NOT NULL, amount INTEGER NOT NULL,
@@ -53,6 +62,8 @@ CREATE TABLE IF NOT EXISTS vtxo (
swept BOOLEAN NOT NULL, swept BOOLEAN NOT NULL,
expire_at INTEGER NOT NULL, expire_at INTEGER NOT NULL,
payment_id TEXT, payment_id TEXT,
redeem_tx TEXT,
PRIMARY KEY (txid, vout),
FOREIGN KEY (payment_id) REFERENCES payment(id) FOREIGN KEY (payment_id) REFERENCES payment(id)
); );
@@ -74,4 +85,9 @@ ON payment.id=receiver.payment_id;
CREATE VIEW payment_vtxo_vw AS SELECT vtxo.* CREATE VIEW payment_vtxo_vw AS SELECT vtxo.*
FROM payment FROM payment
LEFT OUTER JOIN vtxo LEFT OUTER JOIN vtxo
ON payment.id=vtxo.payment_id; ON payment.id=vtxo.payment_id;
CREATE VIEW uncond_forfeit_tx_vw AS SELECT uncond_forfeit_tx.*
FROM vtxo
LEFT OUTER JOIN uncond_forfeit_tx
ON vtxo.txid=uncond_forfeit_tx.vtxo_txid AND vtxo.vout=uncond_forfeit_tx.vtxo_vout;

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.26.0 // sqlc v1.27.0
package queries package queries

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.26.0 // sqlc v1.27.0
package queries package queries
@@ -32,6 +32,7 @@ type PaymentVtxoVw struct {
Swept sql.NullBool Swept sql.NullBool
ExpireAt sql.NullInt64 ExpireAt sql.NullInt64
PaymentID sql.NullString PaymentID sql.NullString
RedeemTx sql.NullString
} }
type Receiver struct { type Receiver struct {
@@ -85,6 +86,22 @@ type Tx struct {
IsLeaf sql.NullBool IsLeaf sql.NullBool
} }
type UncondForfeitTx struct {
ID int64
Tx string
VtxoTxid string
VtxoVout int64
Position int64
}
type UncondForfeitTxVw struct {
ID sql.NullInt64
Tx sql.NullString
VtxoTxid sql.NullString
VtxoVout sql.NullInt64
Position sql.NullInt64
}
type Vtxo struct { type Vtxo struct {
Txid string Txid string
Vout int64 Vout int64
@@ -97,4 +114,5 @@ type Vtxo struct {
Swept bool Swept bool
ExpireAt int64 ExpireAt int64
PaymentID sql.NullString PaymentID sql.NullString
RedeemTx sql.NullString
} }

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.26.0 // sqlc v1.27.0
// source: query.sql // source: query.sql
package queries package queries
@@ -54,30 +54,45 @@ func (q *Queries) MarkVtxoAsSwept(ctx context.Context, arg MarkVtxoAsSweptParams
} }
const selectNotRedeemedVtxos = `-- name: SelectNotRedeemedVtxos :many const selectNotRedeemedVtxos = `-- name: SelectNotRedeemedVtxos :many
SELECT txid, vout, pubkey, amount, pool_tx, spent_by, spent, redeemed, swept, expire_at, payment_id FROM vtxo WHERE redeemed = false 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,
uncond_forfeit_tx_vw.id, uncond_forfeit_tx_vw.tx, uncond_forfeit_tx_vw.vtxo_txid, uncond_forfeit_tx_vw.vtxo_vout, uncond_forfeit_tx_vw.position
FROM vtxo
LEFT OUTER JOIN uncond_forfeit_tx_vw ON vtxo.txid=uncond_forfeit_tx_vw.vtxo_txid AND vtxo.vout=uncond_forfeit_tx_vw.vtxo_vout
WHERE redeemed = false
` `
func (q *Queries) SelectNotRedeemedVtxos(ctx context.Context) ([]Vtxo, error) { type SelectNotRedeemedVtxosRow struct {
Vtxo Vtxo
UncondForfeitTxVw UncondForfeitTxVw
}
func (q *Queries) SelectNotRedeemedVtxos(ctx context.Context) ([]SelectNotRedeemedVtxosRow, error) {
rows, err := q.db.QueryContext(ctx, selectNotRedeemedVtxos) rows, err := q.db.QueryContext(ctx, selectNotRedeemedVtxos)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var items []Vtxo var items []SelectNotRedeemedVtxosRow
for rows.Next() { for rows.Next() {
var i Vtxo var i SelectNotRedeemedVtxosRow
if err := rows.Scan( if err := rows.Scan(
&i.Txid, &i.Vtxo.Txid,
&i.Vout, &i.Vtxo.Vout,
&i.Pubkey, &i.Vtxo.Pubkey,
&i.Amount, &i.Vtxo.Amount,
&i.PoolTx, &i.Vtxo.PoolTx,
&i.SpentBy, &i.Vtxo.SpentBy,
&i.Spent, &i.Vtxo.Spent,
&i.Redeemed, &i.Vtxo.Redeemed,
&i.Swept, &i.Vtxo.Swept,
&i.ExpireAt, &i.Vtxo.ExpireAt,
&i.PaymentID, &i.Vtxo.PaymentID,
&i.Vtxo.RedeemTx,
&i.UncondForfeitTxVw.ID,
&i.UncondForfeitTxVw.Tx,
&i.UncondForfeitTxVw.VtxoTxid,
&i.UncondForfeitTxVw.VtxoVout,
&i.UncondForfeitTxVw.Position,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -93,30 +108,45 @@ func (q *Queries) SelectNotRedeemedVtxos(ctx context.Context) ([]Vtxo, error) {
} }
const selectNotRedeemedVtxosWithPubkey = `-- name: SelectNotRedeemedVtxosWithPubkey :many const selectNotRedeemedVtxosWithPubkey = `-- name: SelectNotRedeemedVtxosWithPubkey :many
SELECT txid, vout, pubkey, amount, pool_tx, spent_by, spent, redeemed, swept, expire_at, payment_id FROM vtxo WHERE redeemed = false AND pubkey = ? 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,
uncond_forfeit_tx_vw.id, uncond_forfeit_tx_vw.tx, uncond_forfeit_tx_vw.vtxo_txid, uncond_forfeit_tx_vw.vtxo_vout, uncond_forfeit_tx_vw.position
FROM vtxo
LEFT OUTER JOIN uncond_forfeit_tx_vw ON vtxo.txid=uncond_forfeit_tx_vw.vtxo_txid AND vtxo.vout=uncond_forfeit_tx_vw.vtxo_vout
WHERE redeemed = false AND pubkey = ?
` `
func (q *Queries) SelectNotRedeemedVtxosWithPubkey(ctx context.Context, pubkey string) ([]Vtxo, error) { type SelectNotRedeemedVtxosWithPubkeyRow struct {
Vtxo Vtxo
UncondForfeitTxVw UncondForfeitTxVw
}
func (q *Queries) SelectNotRedeemedVtxosWithPubkey(ctx context.Context, pubkey string) ([]SelectNotRedeemedVtxosWithPubkeyRow, error) {
rows, err := q.db.QueryContext(ctx, selectNotRedeemedVtxosWithPubkey, pubkey) rows, err := q.db.QueryContext(ctx, selectNotRedeemedVtxosWithPubkey, pubkey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var items []Vtxo var items []SelectNotRedeemedVtxosWithPubkeyRow
for rows.Next() { for rows.Next() {
var i Vtxo var i SelectNotRedeemedVtxosWithPubkeyRow
if err := rows.Scan( if err := rows.Scan(
&i.Txid, &i.Vtxo.Txid,
&i.Vout, &i.Vtxo.Vout,
&i.Pubkey, &i.Vtxo.Pubkey,
&i.Amount, &i.Vtxo.Amount,
&i.PoolTx, &i.Vtxo.PoolTx,
&i.SpentBy, &i.Vtxo.SpentBy,
&i.Spent, &i.Vtxo.Spent,
&i.Redeemed, &i.Vtxo.Redeemed,
&i.Swept, &i.Vtxo.Swept,
&i.ExpireAt, &i.Vtxo.ExpireAt,
&i.PaymentID, &i.Vtxo.PaymentID,
&i.Vtxo.RedeemTx,
&i.UncondForfeitTxVw.ID,
&i.UncondForfeitTxVw.Tx,
&i.UncondForfeitTxVw.VtxoTxid,
&i.UncondForfeitTxVw.VtxoVout,
&i.UncondForfeitTxVw.Position,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -195,7 +225,7 @@ SELECT round.id, round.starting_timestamp, round.ending_timestamp, round.ended,
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.pubkey, payment_receiver_vw.amount, payment_receiver_vw.onchain_address, payment_receiver_vw.payment_id, payment_receiver_vw.pubkey, payment_receiver_vw.amount, payment_receiver_vw.onchain_address,
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.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
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,6 +290,7 @@ func (q *Queries) SelectRoundWithRoundId(ctx context.Context, id string) ([]Sele
&i.PaymentVtxoVw.Swept, &i.PaymentVtxoVw.Swept,
&i.PaymentVtxoVw.ExpireAt, &i.PaymentVtxoVw.ExpireAt,
&i.PaymentVtxoVw.PaymentID, &i.PaymentVtxoVw.PaymentID,
&i.PaymentVtxoVw.RedeemTx,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -279,7 +310,7 @@ SELECT round.id, round.starting_timestamp, round.ending_timestamp, round.ended,
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.pubkey, payment_receiver_vw.amount, payment_receiver_vw.onchain_address, payment_receiver_vw.payment_id, payment_receiver_vw.pubkey, payment_receiver_vw.amount, payment_receiver_vw.onchain_address,
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.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
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
@@ -344,6 +375,7 @@ func (q *Queries) SelectRoundWithRoundTxId(ctx context.Context, txid string) ([]
&i.PaymentVtxoVw.Swept, &i.PaymentVtxoVw.Swept,
&i.PaymentVtxoVw.ExpireAt, &i.PaymentVtxoVw.ExpireAt,
&i.PaymentVtxoVw.PaymentID, &i.PaymentVtxoVw.PaymentID,
&i.PaymentVtxoVw.RedeemTx,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -363,7 +395,7 @@ SELECT round.id, round.starting_timestamp, round.ending_timestamp, round.ended,
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.pubkey, payment_receiver_vw.amount, payment_receiver_vw.onchain_address, payment_receiver_vw.payment_id, payment_receiver_vw.pubkey, payment_receiver_vw.amount, payment_receiver_vw.onchain_address,
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.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
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
@@ -428,6 +460,7 @@ func (q *Queries) SelectSweepableRounds(ctx context.Context) ([]SelectSweepableR
&i.PaymentVtxoVw.Swept, &i.PaymentVtxoVw.Swept,
&i.PaymentVtxoVw.ExpireAt, &i.PaymentVtxoVw.ExpireAt,
&i.PaymentVtxoVw.PaymentID, &i.PaymentVtxoVw.PaymentID,
&i.PaymentVtxoVw.RedeemTx,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -443,30 +476,45 @@ func (q *Queries) SelectSweepableRounds(ctx context.Context) ([]SelectSweepableR
} }
const selectSweepableVtxos = `-- name: SelectSweepableVtxos :many const selectSweepableVtxos = `-- name: SelectSweepableVtxos :many
SELECT txid, vout, pubkey, amount, pool_tx, spent_by, spent, redeemed, swept, expire_at, payment_id FROM vtxo WHERE redeemed = false AND swept = false 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,
uncond_forfeit_tx_vw.id, uncond_forfeit_tx_vw.tx, uncond_forfeit_tx_vw.vtxo_txid, uncond_forfeit_tx_vw.vtxo_vout, uncond_forfeit_tx_vw.position
FROM vtxo
LEFT OUTER JOIN uncond_forfeit_tx_vw ON vtxo.txid=uncond_forfeit_tx_vw.vtxo_txid AND vtxo.vout=uncond_forfeit_tx_vw.vtxo_vout
WHERE redeemed = false AND swept = false
` `
func (q *Queries) SelectSweepableVtxos(ctx context.Context) ([]Vtxo, error) { type SelectSweepableVtxosRow struct {
Vtxo Vtxo
UncondForfeitTxVw UncondForfeitTxVw
}
func (q *Queries) SelectSweepableVtxos(ctx context.Context) ([]SelectSweepableVtxosRow, error) {
rows, err := q.db.QueryContext(ctx, selectSweepableVtxos) rows, err := q.db.QueryContext(ctx, selectSweepableVtxos)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var items []Vtxo var items []SelectSweepableVtxosRow
for rows.Next() { for rows.Next() {
var i Vtxo var i SelectSweepableVtxosRow
if err := rows.Scan( if err := rows.Scan(
&i.Txid, &i.Vtxo.Txid,
&i.Vout, &i.Vtxo.Vout,
&i.Pubkey, &i.Vtxo.Pubkey,
&i.Amount, &i.Vtxo.Amount,
&i.PoolTx, &i.Vtxo.PoolTx,
&i.SpentBy, &i.Vtxo.SpentBy,
&i.Spent, &i.Vtxo.Spent,
&i.Redeemed, &i.Vtxo.Redeemed,
&i.Swept, &i.Vtxo.Swept,
&i.ExpireAt, &i.Vtxo.ExpireAt,
&i.PaymentID, &i.Vtxo.PaymentID,
&i.Vtxo.RedeemTx,
&i.UncondForfeitTxVw.ID,
&i.UncondForfeitTxVw.Tx,
&i.UncondForfeitTxVw.VtxoTxid,
&i.UncondForfeitTxVw.VtxoVout,
&i.UncondForfeitTxVw.Position,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -486,7 +534,7 @@ SELECT round.id, round.starting_timestamp, round.ending_timestamp, round.ended,
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.pubkey, payment_receiver_vw.amount, payment_receiver_vw.onchain_address, payment_receiver_vw.payment_id, payment_receiver_vw.pubkey, payment_receiver_vw.amount, payment_receiver_vw.onchain_address,
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.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
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
@@ -551,6 +599,7 @@ func (q *Queries) SelectSweptRounds(ctx context.Context) ([]SelectSweptRoundsRow
&i.PaymentVtxoVw.Swept, &i.PaymentVtxoVw.Swept,
&i.PaymentVtxoVw.ExpireAt, &i.PaymentVtxoVw.ExpireAt,
&i.PaymentVtxoVw.PaymentID, &i.PaymentVtxoVw.PaymentID,
&i.PaymentVtxoVw.RedeemTx,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -566,7 +615,11 @@ func (q *Queries) SelectSweptRounds(ctx context.Context) ([]SelectSweptRoundsRow
} }
const selectVtxoByOutpoint = `-- name: SelectVtxoByOutpoint :one const selectVtxoByOutpoint = `-- name: SelectVtxoByOutpoint :one
SELECT txid, vout, pubkey, amount, pool_tx, spent_by, spent, redeemed, swept, expire_at, payment_id FROM vtxo WHERE txid = ? AND vout = ? 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,
uncond_forfeit_tx_vw.id, uncond_forfeit_tx_vw.tx, uncond_forfeit_tx_vw.vtxo_txid, uncond_forfeit_tx_vw.vtxo_vout, uncond_forfeit_tx_vw.position
FROM vtxo
LEFT OUTER JOIN uncond_forfeit_tx_vw ON vtxo.txid=uncond_forfeit_tx_vw.vtxo_txid AND vtxo.vout=uncond_forfeit_tx_vw.vtxo_vout
WHERE txid = ? AND vout = ?
` `
type SelectVtxoByOutpointParams struct { type SelectVtxoByOutpointParams struct {
@@ -574,50 +627,76 @@ type SelectVtxoByOutpointParams struct {
Vout int64 Vout int64
} }
func (q *Queries) SelectVtxoByOutpoint(ctx context.Context, arg SelectVtxoByOutpointParams) (Vtxo, error) { type SelectVtxoByOutpointRow struct {
Vtxo Vtxo
UncondForfeitTxVw UncondForfeitTxVw
}
func (q *Queries) SelectVtxoByOutpoint(ctx context.Context, arg SelectVtxoByOutpointParams) (SelectVtxoByOutpointRow, error) {
row := q.db.QueryRowContext(ctx, selectVtxoByOutpoint, arg.Txid, arg.Vout) row := q.db.QueryRowContext(ctx, selectVtxoByOutpoint, arg.Txid, arg.Vout)
var i Vtxo var i SelectVtxoByOutpointRow
err := row.Scan( err := row.Scan(
&i.Txid, &i.Vtxo.Txid,
&i.Vout, &i.Vtxo.Vout,
&i.Pubkey, &i.Vtxo.Pubkey,
&i.Amount, &i.Vtxo.Amount,
&i.PoolTx, &i.Vtxo.PoolTx,
&i.SpentBy, &i.Vtxo.SpentBy,
&i.Spent, &i.Vtxo.Spent,
&i.Redeemed, &i.Vtxo.Redeemed,
&i.Swept, &i.Vtxo.Swept,
&i.ExpireAt, &i.Vtxo.ExpireAt,
&i.PaymentID, &i.Vtxo.PaymentID,
&i.Vtxo.RedeemTx,
&i.UncondForfeitTxVw.ID,
&i.UncondForfeitTxVw.Tx,
&i.UncondForfeitTxVw.VtxoTxid,
&i.UncondForfeitTxVw.VtxoVout,
&i.UncondForfeitTxVw.Position,
) )
return i, err return i, err
} }
const selectVtxosByPoolTxid = `-- name: SelectVtxosByPoolTxid :many const selectVtxosByPoolTxid = `-- name: SelectVtxosByPoolTxid :many
SELECT txid, vout, pubkey, amount, pool_tx, spent_by, spent, redeemed, swept, expire_at, payment_id FROM vtxo WHERE pool_tx = ? 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,
uncond_forfeit_tx_vw.id, uncond_forfeit_tx_vw.tx, uncond_forfeit_tx_vw.vtxo_txid, uncond_forfeit_tx_vw.vtxo_vout, uncond_forfeit_tx_vw.position
FROM vtxo
LEFT OUTER JOIN uncond_forfeit_tx_vw ON vtxo.txid=uncond_forfeit_tx_vw.vtxo_txid AND vtxo.vout=uncond_forfeit_tx_vw.vtxo_vout
WHERE pool_tx = ?
` `
func (q *Queries) SelectVtxosByPoolTxid(ctx context.Context, poolTx string) ([]Vtxo, error) { type SelectVtxosByPoolTxidRow struct {
Vtxo Vtxo
UncondForfeitTxVw UncondForfeitTxVw
}
func (q *Queries) SelectVtxosByPoolTxid(ctx context.Context, poolTx string) ([]SelectVtxosByPoolTxidRow, error) {
rows, err := q.db.QueryContext(ctx, selectVtxosByPoolTxid, poolTx) rows, err := q.db.QueryContext(ctx, selectVtxosByPoolTxid, poolTx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var items []Vtxo var items []SelectVtxosByPoolTxidRow
for rows.Next() { for rows.Next() {
var i Vtxo var i SelectVtxosByPoolTxidRow
if err := rows.Scan( if err := rows.Scan(
&i.Txid, &i.Vtxo.Txid,
&i.Vout, &i.Vtxo.Vout,
&i.Pubkey, &i.Vtxo.Pubkey,
&i.Amount, &i.Vtxo.Amount,
&i.PoolTx, &i.Vtxo.PoolTx,
&i.SpentBy, &i.Vtxo.SpentBy,
&i.Spent, &i.Vtxo.Spent,
&i.Redeemed, &i.Vtxo.Redeemed,
&i.Swept, &i.Vtxo.Swept,
&i.ExpireAt, &i.Vtxo.ExpireAt,
&i.PaymentID, &i.Vtxo.PaymentID,
&i.Vtxo.RedeemTx,
&i.UncondForfeitTxVw.ID,
&i.UncondForfeitTxVw.Tx,
&i.UncondForfeitTxVw.VtxoTxid,
&i.UncondForfeitTxVw.VtxoVout,
&i.UncondForfeitTxVw.Position,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -803,18 +882,44 @@ func (q *Queries) UpsertTransaction(ctx context.Context, arg UpsertTransactionPa
return err return err
} }
const upsertUnconditionalForfeitTx = `-- name: UpsertUnconditionalForfeitTx :exec
INSERT INTO uncond_forfeit_tx (tx, vtxo_txid, vtxo_vout, position)
VALUES (?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET
tx = EXCLUDED.tx,
vtxo_txid = EXCLUDED.vtxo_txid,
vtxo_vout = EXCLUDED.vtxo_vout,
position = EXCLUDED.position
`
type UpsertUnconditionalForfeitTxParams struct {
Tx string
VtxoTxid string
VtxoVout int64
Position int64
}
func (q *Queries) UpsertUnconditionalForfeitTx(ctx context.Context, arg UpsertUnconditionalForfeitTxParams) error {
_, err := q.db.ExecContext(ctx, upsertUnconditionalForfeitTx,
arg.Tx,
arg.VtxoTxid,
arg.VtxoVout,
arg.Position,
)
return err
}
const upsertVtxo = `-- name: UpsertVtxo :exec const upsertVtxo = `-- name: UpsertVtxo :exec
INSERT INTO vtxo (txid, vout, pubkey, amount, pool_tx, spent_by, spent, redeemed, swept, expire_at) INSERT INTO vtxo (txid, vout, pubkey, amount, pool_tx, spent_by, spent, redeemed, swept, expire_at, redeem_tx)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(txid) DO UPDATE SET VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(txid, vout) DO UPDATE SET
vout = excluded.vout, pubkey = EXCLUDED.pubkey,
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, spent = EXCLUDED.spent,
spent = excluded.spent, redeemed = EXCLUDED.redeemed,
redeemed = excluded.redeemed, swept = EXCLUDED.swept,
swept = excluded.swept, expire_at = EXCLUDED.expire_at,
expire_at = excluded.expire_at redeem_tx = EXCLUDED.redeem_tx
` `
type UpsertVtxoParams struct { type UpsertVtxoParams struct {
@@ -828,6 +933,7 @@ type UpsertVtxoParams struct {
Redeemed bool Redeemed bool
Swept bool Swept bool
ExpireAt int64 ExpireAt int64
RedeemTx sql.NullString
} }
func (q *Queries) UpsertVtxo(ctx context.Context, arg UpsertVtxoParams) error { func (q *Queries) UpsertVtxo(ctx context.Context, arg UpsertVtxoParams) error {
@@ -842,6 +948,7 @@ func (q *Queries) UpsertVtxo(ctx context.Context, arg UpsertVtxoParams) error {
arg.Redeemed, arg.Redeemed,
arg.Swept, arg.Swept,
arg.ExpireAt, arg.ExpireAt,
arg.RedeemTx,
) )
return err return err
} }

View File

@@ -111,33 +111,61 @@ SELECT id FROM round WHERE starting_timestamp > ? AND starting_timestamp < ?;
-- name: SelectRoundIds :many -- name: SelectRoundIds :many
SELECT id FROM round; SELECT id FROM round;
-- name: UpsertUnconditionalForfeitTx :exec
INSERT INTO uncond_forfeit_tx (tx, vtxo_txid, vtxo_vout, position)
VALUES (?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET
tx = EXCLUDED.tx,
vtxo_txid = EXCLUDED.vtxo_txid,
vtxo_vout = EXCLUDED.vtxo_vout,
position = EXCLUDED.position;
-- name: UpsertVtxo :exec -- name: UpsertVtxo :exec
INSERT INTO vtxo (txid, vout, pubkey, amount, pool_tx, spent_by, spent, redeemed, swept, expire_at) INSERT INTO vtxo (txid, vout, pubkey, amount, pool_tx, spent_by, spent, redeemed, swept, expire_at, redeem_tx)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(txid) DO UPDATE SET VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(txid, vout) DO UPDATE SET
vout = excluded.vout, pubkey = EXCLUDED.pubkey,
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, spent = EXCLUDED.spent,
spent = excluded.spent, redeemed = EXCLUDED.redeemed,
redeemed = excluded.redeemed, swept = EXCLUDED.swept,
swept = excluded.swept, expire_at = EXCLUDED.expire_at,
expire_at = excluded.expire_at; redeem_tx = EXCLUDED.redeem_tx;
-- name: SelectSweepableVtxos :many -- name: SelectSweepableVtxos :many
SELECT * FROM vtxo WHERE redeemed = false AND swept = false; SELECT sqlc.embed(vtxo),
sqlc.embed(uncond_forfeit_tx_vw)
FROM vtxo
LEFT OUTER JOIN uncond_forfeit_tx_vw ON vtxo.txid=uncond_forfeit_tx_vw.vtxo_txid AND vtxo.vout=uncond_forfeit_tx_vw.vtxo_vout
WHERE redeemed = false AND swept = false;
-- name: SelectNotRedeemedVtxos :many -- name: SelectNotRedeemedVtxos :many
SELECT * FROM vtxo WHERE redeemed = false; SELECT sqlc.embed(vtxo),
sqlc.embed(uncond_forfeit_tx_vw)
FROM vtxo
LEFT OUTER JOIN uncond_forfeit_tx_vw ON vtxo.txid=uncond_forfeit_tx_vw.vtxo_txid AND vtxo.vout=uncond_forfeit_tx_vw.vtxo_vout
WHERE redeemed = false;
-- name: SelectNotRedeemedVtxosWithPubkey :many -- name: SelectNotRedeemedVtxosWithPubkey :many
SELECT * FROM vtxo WHERE redeemed = false AND pubkey = ?; SELECT sqlc.embed(vtxo),
sqlc.embed(uncond_forfeit_tx_vw)
FROM vtxo
LEFT OUTER JOIN uncond_forfeit_tx_vw ON vtxo.txid=uncond_forfeit_tx_vw.vtxo_txid AND vtxo.vout=uncond_forfeit_tx_vw.vtxo_vout
WHERE redeemed = false AND pubkey = ?;
-- name: SelectVtxoByOutpoint :one -- name: SelectVtxoByOutpoint :one
SELECT * FROM vtxo WHERE txid = ? AND vout = ?; SELECT sqlc.embed(vtxo),
sqlc.embed(uncond_forfeit_tx_vw)
FROM vtxo
LEFT OUTER JOIN uncond_forfeit_tx_vw ON vtxo.txid=uncond_forfeit_tx_vw.vtxo_txid AND vtxo.vout=uncond_forfeit_tx_vw.vtxo_vout
WHERE txid = ? AND vout = ?;
-- name: SelectVtxosByPoolTxid :many -- name: SelectVtxosByPoolTxid :many
SELECT * FROM vtxo WHERE pool_tx = ?; SELECT sqlc.embed(vtxo),
sqlc.embed(uncond_forfeit_tx_vw)
FROM vtxo
LEFT OUTER JOIN uncond_forfeit_tx_vw ON vtxo.txid=uncond_forfeit_tx_vw.vtxo_txid AND vtxo.vout=uncond_forfeit_tx_vw.vtxo_vout
WHERE pool_tx = ?;
-- name: MarkVtxoAsRedeemed :exec -- name: MarkVtxoAsRedeemed :exec
UPDATE vtxo SET redeemed = true WHERE txid = ? AND vout = ?; UPDATE vtxo SET redeemed = true WHERE txid = ? AND vout = ?;

View File

@@ -35,10 +35,14 @@ func (v *vxtoRepository) Close() {
func (v *vxtoRepository) AddVtxos(ctx context.Context, vtxos []domain.Vtxo) error { func (v *vxtoRepository) AddVtxos(ctx context.Context, vtxos []domain.Vtxo) error {
txBody := func(querierWithTx *queries.Queries) error { txBody := func(querierWithTx *queries.Queries) error {
for _, vtxo := range vtxos { for i := range vtxos {
vtxo := vtxos[i]
var redeemTx string
if vtxo.AsyncPayment != nil {
redeemTx = vtxo.AsyncPayment.RedeemTx
}
if err := querierWithTx.UpsertVtxo( if err := querierWithTx.UpsertVtxo(
ctx, ctx, queries.UpsertVtxoParams{
queries.UpsertVtxoParams{
Txid: vtxo.Txid, Txid: vtxo.Txid,
Vout: int64(vtxo.VOut), Vout: int64(vtxo.VOut),
Pubkey: vtxo.Pubkey, Pubkey: vtxo.Pubkey,
@@ -49,10 +53,24 @@ func (v *vxtoRepository) AddVtxos(ctx context.Context, vtxos []domain.Vtxo) erro
Redeemed: vtxo.Redeemed, Redeemed: vtxo.Redeemed,
Swept: vtxo.Swept, Swept: vtxo.Swept,
ExpireAt: vtxo.ExpireAt, ExpireAt: vtxo.ExpireAt,
RedeemTx: sql.NullString{String: redeemTx, Valid: true},
}, },
); err != nil { ); err != nil {
return err return err
} }
if vtxo.AsyncPayment != nil {
for i, tx := range vtxo.AsyncPayment.UnconditionalForfeitTxs {
if err := querierWithTx.UpsertUnconditionalForfeitTx(ctx, queries.UpsertUnconditionalForfeitTxParams{
Tx: tx,
VtxoTxid: vtxo.Txid,
VtxoVout: int64(vtxo.VOut),
Position: int64(i),
}); err != nil {
return err
}
}
}
} }
return nil return nil
@@ -62,27 +80,49 @@ func (v *vxtoRepository) AddVtxos(ctx context.Context, vtxos []domain.Vtxo) erro
} }
func (v *vxtoRepository) GetAllSweepableVtxos(ctx context.Context) ([]domain.Vtxo, error) { func (v *vxtoRepository) GetAllSweepableVtxos(ctx context.Context) ([]domain.Vtxo, error) {
rows, err := v.querier.SelectSweepableVtxos(ctx) res, err := v.querier.SelectSweepableVtxos(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
rows := make([]vtxoWithUnconditionalForfeitTxs, 0, len(res))
for _, row := range res {
rows = append(rows, vtxoWithUnconditionalForfeitTxs{
vtxo: row.Vtxo,
tx: row.UncondForfeitTxVw,
})
}
return readRows(rows) return readRows(rows)
} }
func (v *vxtoRepository) GetAllVtxos(ctx context.Context, pubkey string) ([]domain.Vtxo, []domain.Vtxo, error) { func (v *vxtoRepository) GetAllVtxos(ctx context.Context, pubkey string) ([]domain.Vtxo, []domain.Vtxo, error) {
withPubkey := len(pubkey) > 0 withPubkey := len(pubkey) > 0
var rows []queries.Vtxo var rows []vtxoWithUnconditionalForfeitTxs
var err error
if withPubkey { if withPubkey {
rows, err = v.querier.SelectNotRedeemedVtxosWithPubkey(ctx, pubkey) res, err := v.querier.SelectNotRedeemedVtxosWithPubkey(ctx, pubkey)
if err != nil {
return nil, nil, err
}
rows = make([]vtxoWithUnconditionalForfeitTxs, 0, len(res))
for _, row := range res {
rows = append(rows, vtxoWithUnconditionalForfeitTxs{
vtxo: row.Vtxo,
tx: row.UncondForfeitTxVw,
})
}
} else { } else {
rows, err = v.querier.SelectNotRedeemedVtxos(ctx) res, err := v.querier.SelectNotRedeemedVtxos(ctx)
} if err != nil {
if err != nil { return nil, nil, err
return nil, nil, err }
rows = make([]vtxoWithUnconditionalForfeitTxs, 0, len(res))
for _, row := range res {
rows = append(rows, vtxoWithUnconditionalForfeitTxs{
vtxo: row.Vtxo,
tx: row.UncondForfeitTxVw,
})
}
} }
vtxos, err := readRows(rows) vtxos, err := readRows(rows)
@@ -107,7 +147,7 @@ func (v *vxtoRepository) GetAllVtxos(ctx context.Context, pubkey string) ([]doma
func (v *vxtoRepository) GetVtxos(ctx context.Context, outpoints []domain.VtxoKey) ([]domain.Vtxo, error) { func (v *vxtoRepository) GetVtxos(ctx context.Context, outpoints []domain.VtxoKey) ([]domain.Vtxo, error) {
vtxos := make([]domain.Vtxo, 0, len(outpoints)) vtxos := make([]domain.Vtxo, 0, len(outpoints))
for _, o := range outpoints { for _, o := range outpoints {
vtxo, err := v.querier.SelectVtxoByOutpoint( res, err := v.querier.SelectVtxoByOutpoint(
ctx, ctx,
queries.SelectVtxoByOutpointParams{ queries.SelectVtxoByOutpointParams{
Txid: o.Txid, Txid: o.Txid,
@@ -118,7 +158,12 @@ func (v *vxtoRepository) GetVtxos(ctx context.Context, outpoints []domain.VtxoKe
return nil, err return nil, err
} }
result, err := readRows([]queries.Vtxo{vtxo}) result, err := readRows([]vtxoWithUnconditionalForfeitTxs{
{
vtxo: res.Vtxo,
tx: res.UncondForfeitTxVw,
},
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -134,10 +179,17 @@ func (v *vxtoRepository) GetVtxos(ctx context.Context, outpoints []domain.VtxoKe
} }
func (v *vxtoRepository) GetVtxosForRound(ctx context.Context, txid string) ([]domain.Vtxo, error) { func (v *vxtoRepository) GetVtxosForRound(ctx context.Context, txid string) ([]domain.Vtxo, error) {
rows, err := v.querier.SelectVtxosByPoolTxid(ctx, txid) res, err := v.querier.SelectVtxosByPoolTxid(ctx, txid)
if err != nil { if err != nil {
return nil, err return nil, err
} }
rows := make([]vtxoWithUnconditionalForfeitTxs, 0, len(res))
for _, row := range res {
rows = append(rows, vtxoWithUnconditionalForfeitTxs{
vtxo: row.Vtxo,
tx: row.UncondForfeitTxVw,
})
}
return readRows(rows) return readRows(rows)
} }
@@ -224,7 +276,18 @@ func (v *vxtoRepository) UpdateExpireAt(ctx context.Context, vtxos []domain.Vtxo
return execTx(ctx, v.db, txBody) return execTx(ctx, v.db, txBody)
} }
func rowToVtxo(row queries.Vtxo) domain.Vtxo { func rowToVtxo(row queries.Vtxo, uncondForfeitTxs []queries.UncondForfeitTxVw) domain.Vtxo {
var asyncPayment *domain.AsyncPaymentTxs
if row.RedeemTx.Valid && len(uncondForfeitTxs) > 0 {
txs := make([]string, len(uncondForfeitTxs))
for _, tx := range uncondForfeitTxs {
txs[tx.Position.Int64] = tx.Tx.String
}
asyncPayment = &domain.AsyncPaymentTxs{
RedeemTx: row.RedeemTx.String,
UnconditionalForfeitTxs: txs,
}
}
return domain.Vtxo{ return domain.Vtxo{
VtxoKey: domain.VtxoKey{ VtxoKey: domain.VtxoKey{
Txid: row.Txid, Txid: row.Txid,
@@ -234,19 +297,48 @@ func rowToVtxo(row queries.Vtxo) domain.Vtxo {
Pubkey: row.Pubkey, Pubkey: row.Pubkey,
Amount: uint64(row.Amount), Amount: uint64(row.Amount),
}, },
PoolTx: row.PoolTx, PoolTx: row.PoolTx,
SpentBy: row.SpentBy, SpentBy: row.SpentBy,
Spent: row.Spent, Spent: row.Spent,
Redeemed: row.Redeemed, Redeemed: row.Redeemed,
Swept: row.Swept, Swept: row.Swept,
ExpireAt: row.ExpireAt, ExpireAt: row.ExpireAt,
AsyncPayment: asyncPayment,
} }
} }
func readRows(rows []queries.Vtxo) ([]domain.Vtxo, error) { type vtxoWithUnconditionalForfeitTxs struct {
vtxo queries.Vtxo
tx queries.UncondForfeitTxVw
}
func readRows(rows []vtxoWithUnconditionalForfeitTxs) ([]domain.Vtxo, error) {
uncondForfeitTxsMap := make(map[domain.VtxoKey][]queries.UncondForfeitTxVw)
for _, row := range rows {
if !row.vtxo.RedeemTx.Valid {
continue
}
vtxoKey := domain.VtxoKey{
Txid: row.vtxo.Txid,
VOut: uint32(row.vtxo.Vout),
}
if _, ok := uncondForfeitTxsMap[vtxoKey]; !ok {
uncondForfeitTxsMap[vtxoKey] = make([]queries.UncondForfeitTxVw, 0)
}
if row.tx.Tx.Valid {
uncondForfeitTxsMap[vtxoKey] = append(
uncondForfeitTxsMap[vtxoKey], row.tx,
)
}
}
vtxos := make([]domain.Vtxo, 0, len(rows)) vtxos := make([]domain.Vtxo, 0, len(rows))
for _, v := range rows { for _, row := range rows {
vtxos = append(vtxos, rowToVtxo(v)) vtxoKey := domain.VtxoKey{
Txid: row.vtxo.Txid,
VOut: uint32(row.vtxo.Vout),
}
uncondForfeitTxs := uncondForfeitTxsMap[vtxoKey]
vtxos = append(vtxos, rowToVtxo(row.vtxo, uncondForfeitTxs))
} }
return vtxos, nil return vtxos, nil

View File

@@ -316,6 +316,12 @@ func (b *txBuilder) FindLeaves(
return foundLeaves, nil return foundLeaves, nil
} }
func (b *txBuilder) BuildAsyncPaymentTransactions(
_ []domain.Vtxo, _ *secp256k1.PublicKey, _ []domain.Receiver, _ uint64,
) (*domain.AsyncPaymentTxs, error) {
return nil, fmt.Errorf("not implemented")
}
func (b *txBuilder) getLeafScriptAndTree( func (b *txBuilder) getLeafScriptAndTree(
userPubkey, aspPubkey *secp256k1.PublicKey, userPubkey, aspPubkey *secp256k1.PublicKey,
) ([]byte, *taproot.IndexedElementsTapScriptTree, error) { ) ([]byte, *taproot.IndexedElementsTapScriptTree, error) {

View File

@@ -303,6 +303,171 @@ func (b *txBuilder) FindLeaves(congestionTree tree.CongestionTree, fromtxid stri
return foundLeaves, nil return foundLeaves, nil
} }
// TODO add locktimes to txs
func (b *txBuilder) BuildAsyncPaymentTransactions(
vtxos []domain.Vtxo, aspPubKey *secp256k1.PublicKey,
receivers []domain.Receiver, minRelayFee uint64,
) (*domain.AsyncPaymentTxs, error) {
if len(vtxos) <= 0 {
return nil, fmt.Errorf("missing vtxos")
}
ins := make([]*wire.OutPoint, 0, len(vtxos))
outs := make([]*wire.TxOut, 0, len(receivers))
unconditionalForfeitTxs := make([]string, 0, len(vtxos))
for _, vtxo := range vtxos {
if vtxo.Spent {
return nil, fmt.Errorf("all vtxos must be unspent")
}
senderBytes, err := hex.DecodeString(vtxo.Pubkey)
if err != nil {
return nil, err
}
sender, err := secp256k1.ParsePubKey(senderBytes)
if err != nil {
return nil, err
}
aspScript, err := p2trScript(aspPubKey, b.onchainNetwork())
if err != nil {
return nil, err
}
vtxoTxID, err := chainhash.NewHashFromStr(vtxo.Txid)
if err != nil {
return nil, err
}
vtxoOutpoint := &wire.OutPoint{
Hash: *vtxoTxID,
Index: vtxo.VOut,
}
vtxoScript, vtxoTree, err := b.getLeafScriptAndTree(sender, aspPubKey)
if err != nil {
return nil, err
}
output := &wire.TxOut{
PkScript: aspScript,
Value: int64(vtxo.Amount - minRelayFee),
}
forfeitClosure := &bitcointree.MultisigClosure{
Pubkey: sender,
AspPubkey: aspPubKey,
}
forfeitLeaf, err := forfeitClosure.Leaf()
if err != nil {
return nil, err
}
leafProof := vtxoTree.LeafMerkleProofs[vtxoTree.LeafProofIndex[forfeitLeaf.TapHash()]]
ctrlBlock := leafProof.ToControlBlock(bitcointree.UnspendableKey())
ctrlBlockBytes, err := ctrlBlock.ToBytes()
if err != nil {
return nil, err
}
unconditionnalForfeitPtx, err := psbt.New(
[]*wire.OutPoint{vtxoOutpoint},
[]*wire.TxOut{output},
2,
0,
[]uint32{wire.MaxTxInSequenceNum},
)
if err != nil {
return nil, err
}
unconditionnalForfeitPtx.Inputs[0].WitnessUtxo = &wire.TxOut{
Value: int64(vtxo.Amount),
PkScript: vtxoScript,
}
unconditionnalForfeitPtx.Inputs[0].TaprootInternalKey = schnorr.SerializePubKey(bitcointree.UnspendableKey())
unconditionnalForfeitPtx.Inputs[0].TaprootLeafScript = []*psbt.TaprootTapLeafScript{
{
ControlBlock: ctrlBlockBytes,
Script: forfeitLeaf.Script,
LeafVersion: txscript.BaseLeafVersion,
},
}
forfeitTx, err := unconditionnalForfeitPtx.B64Encode()
if err != nil {
return nil, err
}
unconditionalForfeitTxs = append(unconditionalForfeitTxs, forfeitTx)
ins = append(ins, vtxoOutpoint)
}
for i, receiver := range receivers {
// TODO (@louisinger): Add revert policy (sender+ASP)
buf, err := hex.DecodeString(receiver.Pubkey)
if err != nil {
return nil, err
}
receiverPk, err := secp256k1.ParsePubKey(buf)
if err != nil {
return nil, err
}
newVtxoScript, _, err := b.getLeafScriptAndTree(receiverPk, aspPubKey)
if err != nil {
return nil, err
}
// Deduct the min relay fee from the very last receiver which is supposed
// to be the change in case it's not a send-all.
value := receiver.Amount
if i == len(receivers)-1 {
value -= minRelayFee
}
outs = append(outs, &wire.TxOut{
Value: int64(value),
PkScript: newVtxoScript,
})
}
redeemPtx, err := psbt.New(
ins, outs, 2, 0, []uint32{wire.MaxTxInSequenceNum},
)
if err != nil {
return nil, err
}
for i := range redeemPtx.Inputs {
unconditionnalForfeitPsbt, _ := psbt.NewFromRawBytes(
strings.NewReader(unconditionalForfeitTxs[i]), true,
)
redeemPtx.Inputs[i].WitnessUtxo = unconditionnalForfeitPsbt.Inputs[0].WitnessUtxo
redeemPtx.Inputs[i].TaprootInternalKey = unconditionnalForfeitPsbt.Inputs[0].TaprootInternalKey
redeemPtx.Inputs[i].TaprootLeafScript = unconditionnalForfeitPsbt.Inputs[0].TaprootLeafScript
}
redeemTx, err := redeemPtx.B64Encode()
if err != nil {
return nil, err
}
signedRedeemTx, err := b.wallet.SignTransactionTapscript(
context.Background(), redeemTx, nil,
)
if err != nil {
return nil, err
}
return &domain.AsyncPaymentTxs{
RedeemTx: signedRedeemTx,
UnconditionalForfeitTxs: unconditionalForfeitTxs,
}, nil
}
func (b *txBuilder) getLeafScriptAndTree( func (b *txBuilder) getLeafScriptAndTree(
userPubkey, aspPubkey *secp256k1.PublicKey, userPubkey, aspPubkey *secp256k1.PublicKey,
) ([]byte, *txscript.IndexedTapScriptTree, error) { ) ([]byte, *txscript.IndexedTapScriptTree, error) {
@@ -316,7 +481,7 @@ func (b *txBuilder) getLeafScriptAndTree(
return nil, nil, err return nil, nil, err
} }
forfeitClosure := &bitcointree.ForfeitClosure{ forfeitClosure := &bitcointree.MultisigClosure{
Pubkey: userPubkey, Pubkey: userPubkey,
AspPubkey: aspPubkey, AspPubkey: aspPubkey,
} }
@@ -693,7 +858,9 @@ func (b *txBuilder) createConnectors(
func (b *txBuilder) createForfeitTxs( func (b *txBuilder) createForfeitTxs(
aspPubkey *secp256k1.PublicKey, payments []domain.Payment, connectors []*psbt.Packet, minRelayFee uint64, aspPubkey *secp256k1.PublicKey, payments []domain.Payment, connectors []*psbt.Packet, minRelayFee uint64,
) ([]string, error) { ) ([]string, error) {
aspScript, err := p2wpkhScript(aspPubkey, b.onchainNetwork()) // TODO (@louisinger): are we sure about this change?
aspScript, err := p2trScript(aspPubkey, b.onchainNetwork())
// aspScript, err := p2wpkhScript(aspPubkey, b.onchainNetwork())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -719,7 +886,7 @@ func (b *txBuilder) createForfeitTxs(
var forfeitProof *txscript.TapscriptProof var forfeitProof *txscript.TapscriptProof
for _, proof := range vtxoTaprootTree.LeafMerkleProofs { for _, proof := range vtxoTaprootTree.LeafMerkleProofs {
isForfeit, err := (&bitcointree.ForfeitClosure{}).Decode(proof.Script) isForfeit, err := (&bitcointree.MultisigClosure{}).Decode(proof.Script)
if !isForfeit || err != nil { if !isForfeit || err != nil {
continue continue
} }

View File

@@ -10,7 +10,7 @@ import (
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
) )
func p2wpkhScript(publicKey *secp256k1.PublicKey, net *chaincfg.Params) ([]byte, error) { func p2trScript(publicKey *secp256k1.PublicKey, net *chaincfg.Params) ([]byte, error) {
tapKey := txscript.ComputeTaprootKeyNoScript(publicKey) tapKey := txscript.ComputeTaprootKeyNoScript(publicKey)
payment, err := btcutil.NewAddressWitnessPubKeyHash( payment, err := btcutil.NewAddressWitnessPubKeyHash(

View File

@@ -93,7 +93,7 @@ func (s *service) signPsbt(packet *psbt.Packet) ([]uint32, error) {
} }
} }
// TODO: @louisinger shall we delete this code? // TODO (@louisinger): shall we delete this code?
// prevOutputFetcher := wallet.PsbtPrevOutputFetcher(packet) // prevOutputFetcher := wallet.PsbtPrevOutputFetcher(packet)
// sigHashes := txscript.NewTxSigHashes(tx, prevOutputFetcher) // sigHashes := txscript.NewTxSigHashes(tx, prevOutputFetcher)

View File

@@ -177,6 +177,82 @@ func (s *service) Restore(_ context.Context, seed, password string) error {
} }
func (s *service) Unlock(_ context.Context, password string) error { func (s *service) Unlock(_ context.Context, password string) error {
if s.wallet == nil {
pwd := []byte(password)
opt := btcwallet.LoaderWithLocalWalletDB(s.cfg.Datadir, false, time.Minute)
config := btcwallet.Config{
LogDir: s.cfg.Datadir,
PrivatePass: pwd,
PublicPass: pwd,
Birthday: time.Now(),
RecoveryWindow: 0,
NetParams: s.cfg.chainParams(),
LoaderOptions: []btcwallet.LoaderOption{opt},
CoinSelectionStrategy: wallet.CoinSelectionLargest,
ChainSource: s.chainSource,
}
blockCache := blockcache.NewBlockCache(20 * 1024 * 1024)
wallet, err := btcwallet.New(config, blockCache)
if err != nil {
return fmt.Errorf("failed to setup wallet loader: %s", err)
}
if err := wallet.Start(); err != nil {
return fmt.Errorf("failed to start wallet: %s", err)
}
for {
if !wallet.InternalWallet().ChainSynced() {
log.Debug("waiting sync....")
time.Sleep(3 * time.Second)
continue
}
break
}
log.Debugf("chain synced")
addrs, err := wallet.ListAddresses(string(aspKeyAccount), false)
if err != nil {
return err
}
for info, addrs := range addrs {
if info.AccountName != string(aspKeyAccount) {
continue
}
for _, addr := range addrs {
if addr.Internal {
continue
}
splittedPath := strings.Split(addr.DerivationPath, "/")
last := splittedPath[len(splittedPath)-1]
if last == "0" {
decoded, err := btcutil.DecodeAddress(addr.Address, s.cfg.chainParams())
if err != nil {
return err
}
infos, err := wallet.AddressInfo(decoded)
if err != nil {
return err
}
managedPubkeyAddr, ok := infos.(waddrmgr.ManagedPubKeyAddress)
if !ok {
return fmt.Errorf("failed to cast address to managed pubkey address")
}
s.aspTaprootAddr = managedPubkeyAddr
break
}
}
}
s.wallet = wallet
return nil
}
return s.wallet.InternalWallet().Unlock([]byte(password), nil) return s.wallet.InternalWallet().Unlock([]byte(password), nil)
} }

View File

@@ -40,6 +40,48 @@ func NewHandler(service application.Service) arkv1.ArkServiceServer {
return h return h
} }
func (h *handler) CompletePayment(ctx context.Context, req *arkv1.CompletePaymentRequest) (*arkv1.CompletePaymentResponse, error) {
if req.GetSignedRedeemTx() == "" {
return nil, status.Error(codes.InvalidArgument, "missing signed redeem tx")
}
if len(req.GetSignedUnconditionalForfeitTxs()) <= 0 {
return nil, status.Error(codes.InvalidArgument, "missing signed unconditional forfeit txs")
}
if err := h.svc.CompleteAsyncPayment(
ctx, req.GetSignedRedeemTx(), req.GetSignedUnconditionalForfeitTxs(),
); err != nil {
return nil, err
}
return &arkv1.CompletePaymentResponse{}, nil
}
func (h *handler) CreatePayment(ctx context.Context, req *arkv1.CreatePaymentRequest) (*arkv1.CreatePaymentResponse, error) {
vtxosKeys, err := parseInputs(req.GetInputs())
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
receivers, err := parseReceivers(req.GetOutputs())
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
redeemTx, unconditionalForfeitTxs, err := h.svc.CreateAsyncPayment(
ctx, vtxosKeys, receivers,
)
if err != nil {
return nil, err
}
return &arkv1.CreatePaymentResponse{
SignedRedeemTx: redeemTx,
UsignedUnconditionalForfeitTxs: unconditionalForfeitTxs,
}, nil
}
func (h *handler) Onboard(ctx context.Context, req *arkv1.OnboardRequest) (*arkv1.OnboardResponse, error) { func (h *handler) Onboard(ctx context.Context, req *arkv1.OnboardRequest) (*arkv1.OnboardResponse, error) {
if req.GetUserPubkey() == "" { if req.GetUserPubkey() == "" {
return nil, status.Error(codes.InvalidArgument, "missing user pubkey") return nil, status.Error(codes.InvalidArgument, "missing user pubkey")
@@ -98,12 +140,9 @@ func (h *handler) Ping(ctx context.Context, req *arkv1.PingRequest) (*arkv1.Ping
} }
func (h *handler) RegisterPayment(ctx context.Context, req *arkv1.RegisterPaymentRequest) (*arkv1.RegisterPaymentResponse, error) { func (h *handler) RegisterPayment(ctx context.Context, req *arkv1.RegisterPaymentRequest) (*arkv1.RegisterPaymentResponse, error) {
vtxosKeys := make([]domain.VtxoKey, 0, len(req.GetInputs())) vtxosKeys, err := parseInputs(req.GetInputs())
for _, input := range req.GetInputs() { if err != nil {
vtxosKeys = append(vtxosKeys, domain.VtxoKey{ return nil, status.Error(codes.InvalidArgument, err.Error())
Txid: input.GetTxid(),
VOut: input.GetVout(),
})
} }
id, err := h.svc.SpendVtxos(ctx, vtxosKeys) id, err := h.svc.SpendVtxos(ctx, vtxosKeys)
@@ -350,6 +389,13 @@ func (v vtxoList) toProto(hrp string, aspKey *secp256k1.PublicKey) []*arkv1.Vtxo
key, _ := secp256k1.ParsePubKey(buf) key, _ := secp256k1.ParsePubKey(buf)
addr, _ = common.EncodeAddress(hrp, key, aspKey) addr, _ = common.EncodeAddress(hrp, key, aspKey)
} }
var pendingData *arkv1.PendingPayment
if vv.AsyncPayment != nil {
pendingData = &arkv1.PendingPayment{
RedeemTx: vv.AsyncPayment.RedeemTx,
UnconditionalForfeitTxs: vv.AsyncPayment.UnconditionalForfeitTxs,
}
}
list = append(list, &arkv1.Vtxo{ list = append(list, &arkv1.Vtxo{
Outpoint: &arkv1.Input{ Outpoint: &arkv1.Input{
Txid: vv.Txid, Txid: vv.Txid,
@@ -359,13 +405,16 @@ func (v vtxoList) toProto(hrp string, aspKey *secp256k1.PublicKey) []*arkv1.Vtxo
Address: addr, Address: addr,
Amount: vv.Amount, Amount: vv.Amount,
}, },
PoolTxid: vv.PoolTx, PoolTxid: vv.PoolTx,
Spent: vv.Spent, Spent: vv.Spent,
ExpireAt: vv.ExpireAt, ExpireAt: vv.ExpireAt,
SpentBy: vv.SpentBy, SpentBy: vv.SpentBy,
Swept: vv.Swept, Swept: vv.Swept,
PendingData: pendingData,
Pending: pendingData != nil,
}) })
} }
return list return list
} }

View File

@@ -30,6 +30,21 @@ func parseAddress(addr string) (string, *secp256k1.PublicKey, *secp256k1.PublicK
return common.DecodeAddress(addr) return common.DecodeAddress(addr)
} }
func parseInputs(ins []*arkv1.Input) ([]domain.VtxoKey, error) {
if len(ins) <= 0 {
return nil, fmt.Errorf("missing inputs")
}
vtxos := make([]domain.VtxoKey, 0, len(ins))
for _, input := range ins {
vtxos = append(vtxos, domain.VtxoKey{
Txid: input.GetTxid(),
VOut: input.GetVout(),
})
}
return vtxos, 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 {

View File

@@ -149,6 +149,14 @@ func Whitelist() map[string][]bakery.Op {
Entity: EntityArk, Entity: EntityArk,
Action: "write", Action: "write",
}}, }},
fmt.Sprintf("/%s/CreatePayment", arkv1.ArkService_ServiceDesc.ServiceName): {{
Entity: EntityArk,
Action: "write",
}},
fmt.Sprintf("/%s/CompletePayment", arkv1.ArkService_ServiceDesc.ServiceName): {{
Entity: EntityArk,
Action: "write",
}},
fmt.Sprintf("/%s/Check", grpchealth.Health_ServiceDesc.ServiceName): {{ fmt.Sprintf("/%s/Check", grpchealth.Health_ServiceDesc.ServiceName): {{
Entity: EntityHealth, Entity: EntityHealth,
Action: "read", Action: "read",

View File

@@ -109,6 +109,14 @@ func TestSendOffchain(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, json.Unmarshal([]byte(balanceStr), &balance)) require.NoError(t, json.Unmarshal([]byte(balanceStr), &balance))
require.NotZero(t, balance.Offchain.Total) require.NotZero(t, balance.Offchain.Total)
_, err = runClarkCommand("claim", "--password", utils.Password)
require.NoError(t, err)
balanceStr, err = runClarkCommand("balance")
require.NoError(t, err)
require.NoError(t, json.Unmarshal([]byte(balanceStr), &balance))
require.NotZero(t, balance.Offchain.Total)
} }
func TestUnilateralExit(t *testing.T) { func TestUnilateralExit(t *testing.T) {