Files
ark/pkg/client-sdk/wallet/singlekey/liquid_wallet.go
Louis Singer ff96524f22 Ark Notes (#379)
* ark credits

* rename "ecash" --> "ark credit"

* rework note_test.go

* NewFromString

* create several notes

* note repo: rename "push" to "add"

* RegisterInputsForNextRoundRequest: move "notes" to field #3

* use uint64 as note ID

* rename to voucher

* add nostr notification

* nostr notification test and fixes

* bump badger to 4.3

* allow npub to be registered

* rename poolTxID

* add default relays

* Update server/internal/config/config.go

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

* fix RedeemVouchers test

* notification = voucher

* WASM wrappers

* fix arkd voucher cmd

* test_utils.go ignore gosec rule G101

* fix permissions

* rename ALL to notes

* add URI prefix

* note.go : fix signature encoding

* fix decode note.Data

* Update server/internal/infrastructure/notifier/nostr/nostr.go

Co-authored-by: Pietralberto Mazza <18440657+altafan@users.noreply.github.com>

* Update pkg/client-sdk/wasm/browser/wrappers.go

Co-authored-by: Pietralberto Mazza <18440657+altafan@users.noreply.github.com>

* Update server/internal/infrastructure/notifier/nostr/nostr.go

Co-authored-by: Pietralberto Mazza <18440657+altafan@users.noreply.github.com>

* rework note and entity db + sqlite implementations

* NOTIFICATION_PREFIX -> NOTE_URI_PREFIX

* validate NOTE_URI_PREFIX

* Update defaults to convenant-less mainnet (#2)

* config: defaults to convenant-less tx builder

* Drop env var for blockchain scanner

---------

Co-authored-by: altafan <18440657+altafan@users.noreply.github.com>

* add // before URI prefix

* add URI prefix in admin CreateNote

* Fixes

* rework nonces encoding (#4)

* rework nonces encoding

* add a check in Musig2Nonce decode function

* musig2_test: increase number of signers to 20

* musig2.json: add a test case with a 35 leaves tree

* GetEventStream REST rework

* fix round phases time intervals

* [SDK] Use server-side streams in rest client

* Fix history

* make the URI optional

* Updates

* Fix settled txs in history

* fix e2e test

* go work sync in sdk unit test

* fix signMessage in btc and liquid sdk wallets

---------

Co-authored-by: Marco Argentieri <3596602+tiero@users.noreply.github.com>
Co-authored-by: Pietralberto Mazza <18440657+altafan@users.noreply.github.com>
2024-11-15 19:07:33 +01:00

384 lines
8.9 KiB
Go

package singlekeywallet
import (
"bytes"
"context"
"encoding/hex"
"fmt"
"strings"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/pkg/client-sdk/explorer"
"github.com/ark-network/ark/pkg/client-sdk/internal/utils"
"github.com/ark-network/ark/pkg/client-sdk/types"
"github.com/ark-network/ark/pkg/client-sdk/wallet"
walletstore "github.com/ark-network/ark/pkg/client-sdk/wallet/singlekey/store"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/vulpemventures/go-elements/payment"
"github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/transaction"
)
type liquidWallet struct {
*singlekeyWallet
}
func NewLiquidWallet(
configStore types.ConfigStore, walletStore walletstore.WalletStore,
) (wallet.WalletService, error) {
walletData, err := walletStore.GetWallet()
if err != nil {
return nil, err
}
return &liquidWallet{
&singlekeyWallet{
configStore: configStore,
walletStore: walletStore,
walletData: walletData,
},
}, nil
}
func (w *liquidWallet) GetAddresses(
ctx context.Context,
) ([]wallet.DescriptorAddress, []wallet.DescriptorAddress, []wallet.DescriptorAddress, error) {
offchainAddr, boardingAddr, err := w.getAddress(ctx)
if err != nil {
return nil, nil, nil, err
}
encodedOffchainAddr, err := offchainAddr.Address.Encode()
if err != nil {
return nil, nil, nil, err
}
data, err := w.configStore.GetData(ctx)
if err != nil {
return nil, nil, nil, err
}
liquidNet := utils.ToElementsNetwork(data.Network)
vtxoP2TR, err := payment.FromTweakedKey(offchainAddr.Address.VtxoTapKey, &liquidNet, nil)
if err != nil {
return nil, nil, nil, err
}
redemptionAddr, err := vtxoP2TR.TaprootAddress()
if err != nil {
return nil, nil, nil, err
}
offchainAddrs := []wallet.DescriptorAddress{
{
Descriptor: offchainAddr.Descriptor,
Address: encodedOffchainAddr,
},
}
boardingAddrs := []wallet.DescriptorAddress{
{
Descriptor: boardingAddr.Descriptor,
Address: boardingAddr.Address,
},
}
redemptionAddrs := []wallet.DescriptorAddress{
{
Descriptor: offchainAddr.Descriptor,
Address: redemptionAddr,
},
}
return offchainAddrs, boardingAddrs, redemptionAddrs, nil
}
func (w *liquidWallet) NewAddress(
ctx context.Context, _ bool,
) (*wallet.DescriptorAddress, *wallet.DescriptorAddress, error) {
offchainAddr, boardingAddr, err := w.getAddress(ctx)
if err != nil {
return nil, nil, err
}
encodedOffchainAddr, err := offchainAddr.Address.Encode()
if err != nil {
return nil, nil, err
}
return &wallet.DescriptorAddress{
Descriptor: offchainAddr.Descriptor,
Address: encodedOffchainAddr,
}, &wallet.DescriptorAddress{
Descriptor: boardingAddr.Descriptor,
Address: boardingAddr.Address,
}, nil
}
func (w *liquidWallet) NewAddresses(
ctx context.Context, _ bool, num int,
) ([]wallet.DescriptorAddress, []wallet.DescriptorAddress, error) {
offchainAddr, boardingAddr, err := w.getAddress(ctx)
if err != nil {
return nil, nil, err
}
offchainAddrs := make([]wallet.DescriptorAddress, 0, num)
boardingAddrs := make([]wallet.DescriptorAddress, 0, num)
for i := 0; i < num; i++ {
encodedOffchainAddr, err := offchainAddr.Address.Encode()
if err != nil {
return nil, nil, err
}
offchainAddrs = append(offchainAddrs, wallet.DescriptorAddress{
Descriptor: offchainAddr.Descriptor,
Address: encodedOffchainAddr,
})
boardingAddrs = append(boardingAddrs, wallet.DescriptorAddress{
Descriptor: boardingAddr.Descriptor,
Address: boardingAddr.Address,
})
}
return offchainAddrs, boardingAddrs, nil
}
func (s *liquidWallet) SignTransaction(
ctx context.Context, explorerSvc explorer.Explorer, tx string,
) (string, error) {
pset, err := psetv2.NewPsetFromBase64(tx)
if err != nil {
return "", fmt.Errorf("invalid pset: %s", err)
}
updater, err := psetv2.NewUpdater(pset)
if err != nil {
return "", err
}
for i, input := range pset.Inputs {
if input.WitnessUtxo != nil {
continue
}
prevoutTxHex, err := explorerSvc.GetTxHex(chainhash.Hash(input.PreviousTxid).String())
if err != nil {
return "", err
}
prevoutTx, err := transaction.NewTxFromHex(prevoutTxHex)
if err != nil {
return "", err
}
utxo := prevoutTx.Outputs[input.PreviousTxIndex]
if utxo == nil {
return "", fmt.Errorf("witness utxo not found")
}
if err := updater.AddInWitnessUtxo(i, utxo); err != nil {
return "", err
}
if err := updater.AddInSighashType(i, txscript.SigHashDefault); err != nil {
return "", err
}
}
signer, err := psetv2.NewSigner(updater.Pset)
if err != nil {
return "", err
}
storeData, err := s.configStore.GetData(ctx)
if err != nil {
return "", err
}
liquidNet := utils.ToElementsNetwork(storeData.Network)
utx, err := pset.UnsignedTx()
if err != nil {
return "", err
}
prevoutsScripts := make([][]byte, 0)
prevoutsValues := make([][]byte, 0)
prevoutsAssets := make([][]byte, 0)
for _, input := range pset.Inputs {
prevoutsScripts = append(prevoutsScripts, input.WitnessUtxo.Script)
prevoutsValues = append(prevoutsValues, input.WitnessUtxo.Value)
prevoutsAssets = append(prevoutsAssets, input.WitnessUtxo.Asset)
}
serializedPubKey := s.walletData.Pubkey.SerializeCompressed()
for i, input := range pset.Inputs {
if len(input.TapLeafScript) > 0 {
genesis, err := chainhash.NewHashFromStr(liquidNet.GenesisBlockHash)
if err != nil {
return "", err
}
for _, leaf := range input.TapLeafScript {
closure, err := tree.DecodeClosure(leaf.Script)
if err != nil {
return "", err
}
sign := false
switch c := closure.(type) {
case *tree.CSVSigClosure:
sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], serializedPubKey[1:])
case *tree.MultisigClosure:
sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], serializedPubKey[1:])
}
if sign {
hash := leaf.TapHash()
preimage := utx.HashForWitnessV1(
i,
prevoutsScripts,
prevoutsAssets,
prevoutsValues,
txscript.SigHashDefault,
genesis,
&hash,
nil,
)
sig, err := schnorr.Sign(s.privateKey, preimage[:])
if err != nil {
return "", err
}
tapScriptSig := psetv2.TapScriptSig{
PartialSig: psetv2.PartialSig{
PubKey: schnorr.SerializePubKey(s.walletData.Pubkey),
Signature: sig.Serialize(),
},
LeafHash: hash.CloneBytes(),
}
if err := signer.SignTaprootInputTapscriptSig(i, tapScriptSig); err != nil {
return "", err
}
}
}
}
}
for i, input := range pset.Inputs {
if len(input.PartialSigs) > 0 {
valid, err := pset.ValidateInputSignatures(i)
if err != nil {
return "", err
}
if !valid {
return "", fmt.Errorf("invalid signature for input %d", i)
}
}
}
return pset.ToBase64()
}
func (w *liquidWallet) SignMessage(
ctx context.Context, message []byte, pubkey string,
) (string, error) {
if w.IsLocked() {
return "", fmt.Errorf("wallet is locked")
}
walletPubkeyHex := hex.EncodeToString(schnorr.SerializePubKey(w.walletData.Pubkey))
if walletPubkeyHex != pubkey {
return "", fmt.Errorf("pubkey mismatch, cannot sign message")
}
sig, err := schnorr.Sign(w.privateKey, message)
if err != nil {
return "", err
}
return hex.EncodeToString(sig.Serialize()), nil
}
func (w *liquidWallet) getAddress(
ctx context.Context,
) (
*struct {
Address common.Address
Descriptor string
},
*wallet.DescriptorAddress,
error,
) {
if w.walletData == nil {
return nil, nil, fmt.Errorf("wallet not initialized")
}
data, err := w.configStore.GetData(ctx)
if err != nil {
return nil, nil, err
}
liquidNet := utils.ToElementsNetwork(data.Network)
vtxoScript := &tree.DefaultVtxoScript{
Owner: w.walletData.Pubkey,
Asp: data.AspPubkey,
ExitDelay: uint(data.UnilateralExitDelay),
}
vtxoTapKey, _, err := vtxoScript.TapTree()
if err != nil {
return nil, nil, err
}
offchainAddr := &common.Address{
HRP: data.Network.Addr,
Asp: data.AspPubkey,
VtxoTapKey: vtxoTapKey,
}
myPubkeyStr := hex.EncodeToString(schnorr.SerializePubKey(w.walletData.Pubkey))
descriptorStr := strings.ReplaceAll(
data.BoardingDescriptorTemplate, "USER", myPubkeyStr,
)
onboardingScript, err := tree.ParseVtxoScript(descriptorStr)
if err != nil {
return nil, nil, err
}
tapKey, _, err := onboardingScript.TapTree()
if err != nil {
return nil, nil, err
}
p2tr, err := payment.FromTweakedKey(tapKey, &liquidNet, nil)
if err != nil {
return nil, nil, err
}
boardingAddr, err := p2tr.TaprootAddress()
if err != nil {
return nil, nil, err
}
return &struct {
Address common.Address
Descriptor string
}{
Address: *offchainAddr,
Descriptor: vtxoScript.ToDescriptor(),
}, &wallet.DescriptorAddress{
Descriptor: descriptorStr,
Address: boardingAddr,
}, nil
}