wos

package module
v0.0.0-...-ed0191b Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 8, 2024 License: Unlicense Imports: 18 Imported by: 0

README

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())
}

Documentation

Overview

Package wos leverages the Wallet Of Satoshi REST API to process Bitcoin payments.

Wallet Of Satoshi is a custodial bitcoin wallet which is reknowned for its ease of use. WoS does not provide any documentation on their API, so this package is mostly reverse-engineered by studying how the WoS API is used in the wild.

See the README for more info.

Index

Examples

Constants

View Source
const (
	PaymentStatusPaid    PaymentStatus = "PAID"    // The payment has been completed and confirmed.
	PaymentStatusPending PaymentStatus = "PENDING" // An on-chain payment is still confirming.

	PaymentTypeCredit PaymentType = "CREDIT" // A received payment.
	PaymentTypeDebit  PaymentType = "DEBIT"  // A sent payment.

	PaymentCurrencyBitcoin   PaymentCurrency = "BTC"       // On-chain bitcoin.
	PaymentCurrencyLightning PaymentCurrency = "LIGHTNING" // Off-chain lightning network credit.
)
View Source
const BaseURL = "https://www.livingroomofsatoshi.com"

BaseURL is the API URL for the Wallet of Satoshi API.

Variables

View Source
var (
	// ErrInvalidInvoice is returned when an invalid invoice is received.
	ErrInvalidInvoice = errors.New("invalid invoice")

	// ErrNoAmount is returned when paying an invoice which has no amount specified.
	ErrNoAmount = errors.New("no amount specified in invoice")

	// ErrFixedAmount is returned when attempting to pay a fixed-amount invoice
	// as if it were a variable amount invoice.
	ErrFixedAmount = errors.New("invoice specifies a fixed amount")
)

Functions

func CreateWallet

func CreateWallet(ctx context.Context, httpClient *http.Client) (*Wallet, *Credentials, error)

CreateWallet asks the WoS API to create a brand new wallet from scratch. It returns a Wallet which can be used right away, and a set of access Credentials which should be saved in a persistent storage medium so that the wallet can be re-opened later with OpenWallet.

Example
package main

import (
	"context"
	"fmt"

	"github.com/conduition/wos"
)

func main() {
	wallet, creds, err := wos.CreateWallet(context.Background(), nil)
	if err != nil {
		panic(err)
	}

	fmt.Println(wallet.LightningAddress())
	fmt.Printf("API token:  %s\n", creds.APIToken)
	fmt.Printf("API secret: %s\n", creds.APISecret)
}
Output:

Types

type Addresses

type Addresses struct {
	OnChain   string `json:"btcDepositAddress"`
	Lightning string `json:"lightningAddress"`
}

Addresses represents the on-chain and lightning deposit addresses for a Wallet.

type Balance

type Balance struct {
	Confirmed   float64 `json:"btc"`
	Unconfirmed float64 `json:"btcUnconfirmed"`
}

Balance represents a Wallet's balance at a certain point in time.

func (Balance) Total

func (b Balance) Total() float64

Total returns the sum of the confirmed and unconfirmed balances.

type Credentials

type Credentials struct {
	// APISecret is a base58 secret needed for write-access to a wallet.
	// This secret is used to sign any requests to POST endpoints, such
	// as those which create invoices and make payments.
	//
	// APISecret is useless on its own, but if the corresponding APIToken is
	// available, it permits full access to spend funds from a WoS wallet.
	APISecret string `json:"apiSecret"`

	// APIToken is a read-only access token used to fetch balance information,
	// estimate fees, and view the transaction history of a wallet.
	//
	// Without the APISecret, it can only be used to view a WoS wallet, but
	// it cannot modify the wallet's state in any way.
	APIToken string `json:"apiToken"`
}

Credentials represents a full set of credentials for a WoS wallet.

func (Credentials) OpenWallet

func (creds Credentials) OpenWallet(
	ctx context.Context,
	httpClient *http.Client,
) (*Wallet, error)

OpenWallet opens a Wallet using the given http.Client for all API calls.

Example
package main

import (
	"context"
	"fmt"

	"github.com/conduition/wos"
)

func main() {
	creds := wos.Credentials{
		APIToken:  "edcc867c-96ff-4b0d-ba68-165c16071de0",
		APISecret: "91ul0rDKV1gANhQWWyEXhdWaSa6aQwAF",
	}

	wallet, err := creds.OpenWallet(context.Background(), nil)
	if err != nil {
		panic(err)
	}

	fmt.Println(wallet.LightningAddress())

}
Output:

dorsalpuma54@walletofsatoshi.com

func (Credentials) Reader

func (creds Credentials) Reader(httpClient *http.Client) *Reader

Reader builds Generate a Reader object from the APIToken.

HTTP API calls made by the reader will be executed by the given http.Client.

func (Credentials) SimpleSigner

func (creds Credentials) SimpleSigner() *SimpleSigner

SimpleSigner returns a SimpleSigner which signs using the APISecret.

type FeeEstimate

type FeeEstimate struct {
	BtcFixedFee              float64 `json:"btcFixedFee"`
	BtcMinerFeePerKB         float64 `json:"btcMinerFeePerKb"`
	BtcSendCommissionPercent float64 `json:"btcSendCommissionPercent"`
	BtcSendFeeWarningPercent float64 `json:"btcSendFeeWarningPercent"`
	LightningFee             float64 `json:"lightningFee"`
	MaxLightningFee          float64 `json:"sendMaxLightningFee"`
	IsWosInvoice             bool    `json:"wosInvoice"`
}

type Invoice

type Invoice struct {
	// ID is a UUID which identifies the invoice.
	ID string `json:"id"`

	// Bolt11 is the [BOLT11] serialized invoice. This is what should
	// be displayed to the user or encoded in QR codes.
	Bolt11 string `json:"invoice"`

	// Amount is the Bitcoin amount encoded in the invoice.
	Amount float64 `json:"btcAmount"`

	// Expires is the expiry time at which the invoice is no longer payable.
	Expires time.Time `json:"expires"`
}

Invoice is a Bitcoin Lightning invoice returned by the WoS API.

type InvoiceOptions

type InvoiceOptions struct {
	// Amount is the Bitcoin-denominated amount for the invoice. If not specified,
	// the payee can decide how much to pay.
	Amount float64

	// Description to include in the invoice for the payee. If omitted,
	// no description will be provided to the payee.
	Description string

	// The expiry time for the invoice, after which it can no longer be paid.
	// If omitted, defaults to 24 hours.
	Expiry time.Duration
}

InvoiceOptions is used to customize invoices created by Wallet.NewInvoice.

type Payment

type Payment struct {
	// A UUID identifying the payment.
	ID string `json:"id"`

	// For on-chain bitcoin, this is the address the payment was sent to.
	// For lightning, this is the invoice or LN address.
	Address string `json:"address"`

	// Amount is the Bitcoin-denominated amount of the payment.
	Amount float64 `json:"amount"`

	// Currency is either PaymentCurrencyBitcoin or PaymentCurrencyLightning.
	Currency PaymentCurrency `json:"currency"`

	// Invoice description, or empty string otherwise.
	Description string `json:"description"`

	// Invoice expiry time. Empty for debits.
	Expires time.Time `json:"expires"`

	// If WoS thinks this payment is spam.
	IsLikelySpam bool `json:"isLikelySpam"`

	// If the payment came from the WOS point-of-sale system.
	IsPointOfSale bool `json:"isWosPos"`

	// Time the payment occurred.
	Time time.Time `json:"time"`

	// For lightning payments, this is the payment hash.
	Txid string `json:"transactionId"`

	// Status is either PaymentStatusPaid or PaymentStatusPending.
	// More statuses may exist.
	Status PaymentStatus `json:"status"`

	// Type is either PaymentTypeCredit or PaymentTypeDebit.
	Type PaymentType `json:"type"`
}

Payment represents an on-chain or lightning payment, either received or sent.

type PaymentCurrency

type PaymentCurrency string

PaymentCurrency indicates whether a payment was made on-chain or via Lightning.

type PaymentStatus

type PaymentStatus string

PaymentStatus represents the status of a Payment.

type PaymentType

type PaymentType string

PaymentType indicates whether a payment was incoming or outgoing from a wallet.

type Reader

type Reader struct {
	// contains filtered or unexported fields
}

Reader facilitates read-only access to a WoS wallet. It can be used to fetch balances, payment history, and estimate fees.

Example
package main

import (
	"context"
	"fmt"

	"github.com/conduition/wos"
)

func main() {
	reader := wos.NewReader("edcc867c-96ff-4b0d-ba68-165c16071de0", nil)
	addresses, err := reader.Addresses(context.Background())
	if err != nil {
		panic(err)
	}

	fmt.Println(addresses.Lightning)

}
Output:

dorsalpuma54@walletofsatoshi.com

func NewReader

func NewReader(apiToken string, httpClient *http.Client) *Reader

NewReader constructs a Reader from a given http.Client and read-only apiToken.

Uses http.DefaultClient if httpClient is nil.

func (*Reader) Addresses

func (rdr *Reader) Addresses(ctx context.Context) (*Addresses, error)

Addresses re-fetches the wallet's on-chain and lightning addresses. This can be useful to ensure you have the wallet's latest unused on-chain deposit address.

func (*Reader) Balance

func (rdr *Reader) Balance(ctx context.Context) (*Balance, error)

Balance returns the current confirmed and unconfirmed balances of the wallet.

func (*Reader) BalanceAndFee

func (rdr *Reader) BalanceAndFee(
	ctx context.Context,
	addressOrInvoice string,
) (*Balance, *FeeEstimate, error)

func (*Reader) FeeEstimate

func (rdr *Reader) FeeEstimate(ctx context.Context, addressOrInvoice string) (*FeeEstimate, error)

FeeEstimate fetches the latest fee estimation data when paying to a given on-chain address or lightning invoice.

func (*Reader) GetRequest

func (rdr *Reader) GetRequest(ctx context.Context, endpoint string) ([]byte, error)

GetRequest issues a GET request to the given endpoint, authenticated with the Reader's API token.

func (*Reader) ListPayments

func (rdr *Reader) ListPayments(ctx context.Context) ([]Payment, error)

type Signer

type Signer interface {
	// SignRequest should return a SHA256 HMAC on the following string:
	//
	// 	endpoint + nonce + apiToken + requestBody
	//
	// SignRequest may also perform validation or introspection on the request
	// and decide whether to sign it.
	//
	// If the Signer does not want to sign the request, it should return an error
	// which will be wrapped and bubbled up to the higher-level [Wallet] method.
	SignRequest(ctx context.Context, endpoint, nonce, apiToken, requestBody string) ([]byte, error)
}

Signer represents an HMAC-SHA256 signer which signs the given HTTP request details using the APISecret from the WoS Credentials.

For a simple instantiation of Signer, see SimpleSigner.

Example
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())
}
Output:

type SimpleSigner

type SimpleSigner struct {
	// contains filtered or unexported fields
}

SimpleSigner implements Signer with a static secret and no validation.

Use Credentials.SimpleSigner to create a SimpleSigner from a set of credentials.

func NewSimpleSigner

func NewSimpleSigner(apiSecret string) *SimpleSigner

NewSimpleSigner creates a SimpleSigner from a given APISecret

func (*SimpleSigner) SignRequest

func (s *SimpleSigner) SignRequest(
	ctx context.Context,
	endpoint, nonce, apiToken, requestBody string,
) ([]byte, error)

SignRequest implements Signer.

type Wallet

type Wallet struct {
	// contains filtered or unexported fields
}

Wallet represents a Wallet of Satoshi wallet, including the mechanisms needed to read its history and balances, create invoices, and make payments.

To create a brand new wallet from scratch, use CreateWallet.

To open an existing wallet from a set of Credentials, use Credentials.OpenWallet.

To open a wallet from an isolated signing mechanism, use OpenWallet with a given Signer.

func OpenWallet

func OpenWallet(ctx context.Context, reader *Reader, signer Signer) (*Wallet, error)

OpenWallet opens an existing wallet using a separate Reader and Signer.

The Reader will be used to fetch read-only information about the wallet, while the Signer authenticates write calls.

func (*Wallet) Addresses

func (wallet *Wallet) Addresses(ctx context.Context) (*Addresses, error)

Addresses re-fetches the wallet's on-chain and lightning addresses. This can be useful to ensure you have the wallet's latest unused on-chain deposit address.

func (*Wallet) Balance

func (wallet *Wallet) Balance(ctx context.Context) (*Balance, error)

Balance returns the current confirmed and unconfirmed balances of the wallet.

func (*Wallet) FeeEstimate

func (wallet *Wallet) FeeEstimate(ctx context.Context, addressOrInvoice string) (*FeeEstimate, error)

FeeEstimate fetches the latest fee estimation data when paying to a given on-chain address or lightning invoice.

func (*Wallet) LightningAddress

func (wallet *Wallet) LightningAddress() string

LightningAddress returns the wallet's static Lightning Address.

func (*Wallet) NewInvoice

func (wallet *Wallet) NewInvoice(ctx context.Context, opts *InvoiceOptions) (*Invoice, error)

NewInvoice creates a new BOLT11 payment invoice, essentially a request for payment.

The InvoiceOptions argument customizes the invoice. opts can be nil, which creates a variable-amount invoice with no description and a 24-hour expiry.

func (*Wallet) OnChainAddress

func (wallet *Wallet) OnChainAddress() string

OnChainAddress returns the wallet's on-chain deposit address. Be aware this address might be reused, which is sub-optimal for privacy. To fetch an up-to-date address, use Wallet.Addresses, or re-open the wallet.

func (*Wallet) PayInvoice

func (wallet *Wallet) PayInvoice(ctx context.Context, invoice, description string) (*Payment, error)

PayInvoice executes a payment to a given lightning invoice. The description is stored in the WoS payment history.

Returns an error wrapping ErrInvalidInvoice if the invoice is not valid.

If the invoice does not specify a fixed amount, this method returns an error wrapping ErrNoAmount. To pay a variable-amount invoice, use Wallet.PayVariableInvoice.

To estimate fees, use Wallet.FeeEstimate or Reader.FeeEstimate.

func (*Wallet) PayOnChain

func (wallet *Wallet) PayOnChain(
	ctx context.Context,
	address string,
	amount float64,
	description string,
) (*Payment, error)

PayOnChain executes an on-chain payment transaction, paying amount to the given address. The description is stored in the WoS payment history.

To estimate fees, use Wallet.FeeEstimate or Reader.FeeEstimate.

func (*Wallet) PayVariableInvoice

func (wallet *Wallet) PayVariableInvoice(
	ctx context.Context,
	invoice string,
	description string,
	amount float64,
) (*Payment, error)

PayVariableInvoice executes a payment to a given variable-amount lightning invoice. The description is stored in the WoS payment history.

Returns an error wrapping ErrInvalidInvoice if the invoice is not valid.

Returns an error wrapping ErrFixedAmount if the invoice specifies a fixed amount. In this case, you should use Wallet.PayInvoice.

To estimate fees, use Wallet.FeeEstimate or Reader.FeeEstimate.

func (*Wallet) PostRequest

func (wallet *Wallet) PostRequest(ctx context.Context, endpoint string, body any) ([]byte, error)

PostRequest issues an HTTP POST request to the given endpoint, authenticated by the Wallet's internal Signer. The body parameter is marshaled to JSON and sent as the request body.

func (*Wallet) SetHTTPClient

func (wallet *Wallet) SetHTTPClient(httpClient *http.Client)

SetHTTPClient updates the http.Client used by the wallet and its internal Reader.

func (*Wallet) SweepLightning

func (wallet *Wallet) SweepLightning(ctx context.Context, invoice, description string) (*Payment, error)

SweepLightning executes a lightning payment, sweeping the entire available lightning balance to a given variable-amount invoice. The description is stored in the WoS payment history.

Returns an error wrapping ErrInvalidInvoice if the invoice is not valid.

Returns an error wrapping ErrFixedAmount if the invoice embeds a fixed amount.

func (*Wallet) SweepOnChain

func (wallet *Wallet) SweepOnChain(ctx context.Context, address, description string) (*Payment, error)

SweepOnChain executes an on-chain payment transaction, sweeping the entire available wallet balance to a given on-chain address. The description is stored in the WoS payment history.

Directories

Path Synopsis
Package bech32 provides a Go implementation of the bech32 format specified in BIP 173.
Package bech32 provides a Go implementation of the bech32 format specified in BIP 173.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL