Skip to content

Commit

Permalink
[SDK] add WithEventsCh settle option (#450)
Browse files Browse the repository at this point in the history
* [SDK] add WithEventCh settle option

* WASM: make the callback optional

* WASM: add callback in redeemNotes wrapper

* fix nak.Dockerfile

* typo fix
  • Loading branch information
louisinger authored Feb 7, 2025
1 parent a9b2b18 commit b8ba288
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 18 deletions.
2 changes: 1 addition & 1 deletion nak.Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Use official Go image as base
FROM golang:1.23.1-alpine
FROM golang:1.23.3-alpine

# Install git (needed for go install)
RUN apk add --no-cache git
Expand Down
45 changes: 33 additions & 12 deletions pkg/client-sdk/covenantless_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,31 @@ import (
"golang.org/x/exp/slices"
)

// Musig2SignOptions is only available for covenantless clients
// SettleOptions is only available for covenantless clients
// it allows to customize the vtxo signing process
type Musig2SignOptions struct {
type SettleOptions struct {
ExtraSignerSessions []bitcointree.SignerSession
SigningType *tree.SigningType
WalletSignerDisabled bool

EventsCh chan<- client.RoundEvent
}

func WithEventsCh(ch chan<- client.RoundEvent) Option {
return func(o interface{}) error {
opts, ok := o.(*SettleOptions)
if !ok {
return fmt.Errorf("invalid options type")
}

opts.EventsCh = ch
return nil
}
}

// WithoutWalletSigner disables the wallet signer
func WithoutWalletSigner(o interface{}) error {
opts, ok := o.(*Musig2SignOptions)
opts, ok := o.(*SettleOptions)
if !ok {
return fmt.Errorf("invalid options type")
}
Expand All @@ -57,7 +71,7 @@ func WithoutWalletSigner(o interface{}) error {

// WithSignAll sets the signing type to ALL instead of the default BRANCH
func WithSignAll(o interface{}) error {
opts, ok := o.(*Musig2SignOptions)
opts, ok := o.(*SettleOptions)
if !ok {
return fmt.Errorf("invalid options type")
}
Expand All @@ -70,7 +84,7 @@ func WithSignAll(o interface{}) error {
// WithExtraSigner allows to use a set of custom signer for the vtxo tree signing process
func WithExtraSigner(signerSessions ...bitcointree.SignerSession) Option {
return func(o interface{}) error {
opts, ok := o.(*Musig2SignOptions)
opts, ok := o.(*SettleOptions)
if !ok {
return fmt.Errorf("invalid options type")
}
Expand Down Expand Up @@ -884,7 +898,7 @@ func (a *covenantlessArkClient) SendOffChain(
func (a *covenantlessArkClient) RedeemNotes(ctx context.Context, notes []string, opts ...Option) (string, error) {
amount := uint64(0)

options := &Musig2SignOptions{}
options := &SettleOptions{}
for _, opt := range opts {
if err := opt(options); err != nil {
return "", err
Expand Down Expand Up @@ -939,7 +953,7 @@ func (a *covenantlessArkClient) RedeemNotes(ctx context.Context, notes []string,
log.Infof("payout registered with id: %s", requestID)

roundTxID, err := a.handleRoundStream(
ctx, requestID, nil, nil, receiversOutput, signerSessions,
ctx, requestID, nil, nil, receiversOutput, signerSessions, options.EventsCh,
)
if err != nil {
return "", err
Expand Down Expand Up @@ -1012,7 +1026,7 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
addr string, amount uint64, withExpiryCoinselect bool,
opts ...Option,
) (string, error) {
options := &Musig2SignOptions{}
options := &SettleOptions{}
for _, opt := range opts {
if err := opt(options); err != nil {
return "", err
Expand Down Expand Up @@ -1131,7 +1145,7 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
}

roundTxID, err := a.handleRoundStream(
ctx, requestID, selectedCoins, selectedBoardingCoins, receivers, signerSessions,
ctx, requestID, selectedCoins, selectedBoardingCoins, receivers, signerSessions, options.EventsCh,
)
if err != nil {
return "", err
Expand Down Expand Up @@ -1417,7 +1431,7 @@ func (a *covenantlessArkClient) sendOffchain(
settleOpts ...Option,
) (string, error) {

options := &Musig2SignOptions{}
options := &SettleOptions{}
for _, opt := range settleOpts {
if err := opt(options); err != nil {
return "", err
Expand Down Expand Up @@ -1581,7 +1595,7 @@ func (a *covenantlessArkClient) sendOffchain(
log.Infof("registered inputs and outputs with request id: %s", requestID)

roundTxID, err := a.handleRoundStream(
ctx, requestID, selectedCoins, selectedBoardingCoins, outputs, signerSessions,
ctx, requestID, selectedCoins, selectedBoardingCoins, outputs, signerSessions, options.EventsCh,
)
if err != nil {
return "", err
Expand Down Expand Up @@ -1669,6 +1683,7 @@ func (a *covenantlessArkClient) handleRoundStream(
boardingUtxos []types.Utxo,
receivers []client.Output,
signerSessions []bitcointree.SignerSession,
replayEventsCh chan<- client.RoundEvent,
) (string, error) {
round, err := a.client.GetRound(ctx, "")
if err != nil {
Expand Down Expand Up @@ -1704,9 +1719,15 @@ func (a *covenantlessArkClient) handleRoundStream(
case <-ctx.Done():
return "", fmt.Errorf("context done %s", ctx.Err())
case notify := <-eventsCh:

if notify.Err != nil {
return "", notify.Err
}
if replayEventsCh != nil {
go func() {
replayEventsCh <- notify.Event
}()
}
switch event := notify.Event; event.(type) {
case client.RoundFinalizedEvent:
if step != roundFinalization {
Expand Down Expand Up @@ -2921,7 +2942,7 @@ func buildRedeemTx(
return bitcointree.BuildRedeemTx(ins, outs)
}

func (a *covenantlessArkClient) handleOptions(options *Musig2SignOptions, inputs []client.Input, notesInputs []string) ([]bitcointree.SignerSession, []string, tree.SigningType, error) {
func (a *covenantlessArkClient) handleOptions(options *SettleOptions, inputs []client.Input, notesInputs []string) ([]bitcointree.SignerSession, []string, tree.SigningType, error) {
var signingType tree.SigningType
if options.SigningType != nil {
signingType = *options.SigningType
Expand Down
2 changes: 1 addition & 1 deletion pkg/client-sdk/test/wasm/wasm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ func settle(page playwright.Page) (string, error) {
result, err := page.Evaluate(`async () => {
try {
await unlock("pass");
return await settle();
return await settle((e) => console.log(JSON.parse(e)));
} catch (err) {
console.error("Error:", err);
throw err;
Expand Down
113 changes: 109 additions & 4 deletions pkg/client-sdk/wasm/browser/wrappers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package browser

import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
Expand Down Expand Up @@ -255,11 +256,39 @@ func SendOffChainWrapper() js.Func {

func SettleWrapper() js.Func {
return JSPromise(func(args []js.Value) (interface{}, error) {
if len(args) != 0 {
if len(args) > 1 {
return nil, errors.New("invalid number of args")
}

resp, err := arkSdkClient.Settle(context.Background())
var callback js.Value
if len(args) == 1 && !args[0].IsUndefined() && !args[0].IsNull() {
if args[0].Type() != js.TypeFunction {
return nil, errors.New("callback must be a function")
}
callback = args[0]
}

opts := make([]arksdk.Option, 0)

if !callback.IsUndefined() && !callback.IsNull() {
eventsCh := make(chan client.RoundEvent)
defer close(eventsCh)
go func() {
for event := range eventsCh {
// Transform event to map before marshaling
eventMap := transformEventForJS(event)
jsonEvent, err := json.Marshal(eventMap)
if err != nil {
return
}
callback.Invoke(string(jsonEvent))
}
}()

opts = append(opts, arksdk.WithEventsCh(eventsCh))
}

resp, err := arkSdkClient.Settle(context.Background(), opts...)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -416,7 +445,7 @@ func GetVersionWrapper() js.Func {

func RedeemNotesWrapper() js.Func {
return JSPromise(func(args []js.Value) (interface{}, error) {
if len(args) != 1 {
if len(args) < 1 {
return nil, errors.New("invalid number of args")
}

Expand All @@ -431,7 +460,32 @@ func RedeemNotesWrapper() js.Func {
notes = append(notes, jsNotes.Index(i).String())
}

txID, err := arkSdkClient.RedeemNotes(context.Background(), notes)
opts := make([]arksdk.Option, 0)

// Check for callback in second argument
if len(args) > 1 && !args[1].IsUndefined() && !args[1].IsNull() {
callback := args[1]
if callback.Type() != js.TypeFunction {
return nil, errors.New("callback must be a function")
}

eventsCh := make(chan client.RoundEvent)
defer close(eventsCh)
go func() {
for event := range eventsCh {
eventMap := transformEventForJS(event)
jsonEvent, err := json.Marshal(eventMap)
if err != nil {
return
}
callback.Invoke(string(jsonEvent))
}
}()

opts = append(opts, arksdk.WithEventsCh(eventsCh))
}

txID, err := arkSdkClient.RedeemNotes(context.Background(), notes, opts...)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -555,3 +609,54 @@ func parseOutpoints(jsOutpoints js.Value) ([]client.Outpoint, error) {

return outpoints, nil
}

func transformEventForJS(event client.RoundEvent) map[string]interface{} {
switch e := event.(type) {
case client.RoundFinalizationEvent:
return map[string]interface{}{
"type": "finalization",
"id": e.ID,
"tx": e.Tx,
"tree": e.Tree,
"connectors": e.Connectors,
"minRelayFeeRate": e.MinRelayFeeRate,
}
case client.RoundFinalizedEvent:
return map[string]interface{}{
"type": "finalized",
"id": e.ID,
"txid": e.Txid,
}
case client.RoundFailedEvent:
return map[string]interface{}{
"type": "failed",
"id": e.ID,
"reason": e.Reason,
}
case client.RoundSigningStartedEvent:
return map[string]interface{}{
"type": "signing_started",
"id": e.ID,
"unsignedTree": e.UnsignedTree,
"unsignedRoundTx": e.UnsignedRoundTx,
"cosignersPubkeys": e.CosignersPubkeys,
}
case client.RoundSigningNoncesGeneratedEvent:
var nonceBuffer bytes.Buffer
if err := e.Nonces.Encode(&nonceBuffer); err != nil {
return map[string]interface{}{
"type": "error",
"error": "failed to encode nonces",
}
}
return map[string]interface{}{
"type": "signing_nonces_generated",
"id": e.ID,
"nonces": hex.EncodeToString(nonceBuffer.Bytes()),
}
default:
return map[string]interface{}{
"type": "unknown",
}
}
}

0 comments on commit b8ba288

Please sign in to comment.