Skip to content
/ wos Public

API client for the "Wallet of Satoshi" Bitcoin Lightning app.

License

Notifications You must be signed in to change notification settings

conduition/wos

Folders and files

NameName
Last commit message
Last commit date

Latest commit

73f3eda · Jun 1, 2024

History

4 Commits
Feb 8, 2024
Feb 8, 2024
Feb 8, 2024
Feb 8, 2024
Feb 8, 2024
Feb 8, 2024
Jun 1, 2024
Feb 8, 2024
Feb 8, 2024
Jun 1, 2024
Feb 8, 2024
Feb 8, 2024
Jun 1, 2024

Repository files navigation

wos

API client for the Wallet of Satoshi Bitcoin Lightning app.

Wallet of Satoshi is a custodial Bitcoin Lightning wallet app. It is effectively a web-wallet, because the signing keys are actually hosted on WoS servers, while their mobile app is just a thin API client around their backend sevice.

By using WoS, Bitcoiners trade security for ease-of-use. WoS is well known for being a very beginner-friendly Lightning wallet, due largely to this trade-off. WoS can run off with your money, but you also don't have to worry about running a node, managing channels, updating software, and so forth.

Since WoS is a no-KYC no-signup-required web-wallet, it is very easy to reverse-engineer their API for programmatic use. New wallets can be created on-the-fly with no API credentials needed. Existing wallets can be accessed using simple API credentials.

This library is a Golang package which encapsulates the WoS v1 REST API.

Usage

The Wallet struct type provides a full interface to the WoS API, including creating invoices and sending payments both on-chain and over Lightning.

package main

import (
  "context"
  "fmt"
  "os"

  "github.com/conduition/wos"
)

func main() {
  ctx := context.Background()

  // First, create a wallet from scratch. It will have empty balances
  // but you can start depositing right away via lightning.
  wallet, creds, err := wos.CreateWallet(ctx, nil)
  if err != nil {
    panic(err)
  }
  fmt.Println(wallet.LightningAddress())

  // The Credentials should be saved somewhere, so that you can
  // regain access to the same wallet later.
  os.WriteFile(
    "/secure/location/wos-creds",
    []byte(creds.APIToken+"\n"+creds.APISecret),
    0o600,
  )

  // To reopen the wallet after going offline, parse the Credentials
  // from the disk, and then use Credentials.OpenWallet.
  wallet, err = creds.OpenWallet(ctx, nil)
  if err != nil {
    panic(err)
  }

  // Create an invoice.
  invoice, err := wallet.NewInvoice(ctx, &wos.InvoiceOptions{
    Amount:      0.0001,
    Description: "don't actually send money to this invoice.",
  })
  if err != nil {
    panic(err)
  }
  fmt.Println(invoice.Bolt11)

  // Pay an invoice.
  payment, err := wallet.PayInvoice(ctx, invoice.Bolt11, "a payment label, can be omitted")
  if err != nil {
    panic(err)
  }
  fmt.Println(payment.Status, payment.Amount, payment.Currency, payment.Time)
}

Segregated Credentials

WoS credentials are split into a bearer API token and a shared API secret.

The token is passed as a header with every HTTP request to the WoS API, while the secret is used to produce HMACs for POST requests.

The secret-signature is only required for POST requests which change wallet state - such as creating or paying invoices, GET requests - such as fetching balance or payment history - require only the API token. This means a WoS API client can be segregated into a Reader and a Signer.

A Reader can view a WoS account's balances and ongoing payments in real-time, while A Signer is an interface type which can be a simple wrapper around the API Secret, or the API secret could live offline or on a more secure machine which validates & signs POST requests, enforcing arbitrary user-defined rules (e.g. only allow max $50 per purchase, or max $1000 per day, etc). Put both together and you get a Wallet.

The wos package fully supports this kind of architecture. For example, consider this example with a Signer which lives on a remote machine. Signatures are fetched via HTTP POST requests.

package main

import (
  "bytes"
  "context"
  "encoding/json"
  "fmt"
  "io"
  "net/http"

  "github.com/conduition/wos"
)

type RemoteSigner struct {
  URL string
}

func (rs RemoteSigner) SignRequest(
  ctx context.Context,
  endpoint, nonce, requestBody, apiToken string,
) ([]byte, error) {
  bodyBytes, err := json.Marshal(map[string]string{
    "endpoint": endpoint,
    "nonce":    nonce,
    "body":     requestBody,
  })
  if err != nil {
    return nil, err
  }

  req, err := http.NewRequestWithContext(ctx, "POST", rs.URL, bytes.NewReader(bodyBytes))
  if err != nil {
    return nil, err
  }
  req.Header.Set("Content-Type", "application/json")

  resp, err := http.DefaultClient.Do(req)
  if err != nil {
    return nil, err
  }
  defer resp.Body.Close()

  if resp.StatusCode != 200 {
    return nil, fmt.Errorf("received status code %d from remote signer", resp.StatusCode)
  }

  return io.ReadAll(resp.Body)
}

func main() {
  reader := wos.NewReader("93b9c574-30a2-4bf5-81ba-f9feadb313a7", nil)
  signer := RemoteSigner{"https://somewheresecure.place/api/sign"}
  wallet, err := wos.OpenWallet(context.Background(), reader, signer)
  if err != nil {
    panic(err)
  }
  fmt.Println(wallet.LightningAddress())
}

About

API client for the "Wallet of Satoshi" Bitcoin Lightning app.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Languages