yuri

package module
v0.0.13 Latest Latest
Warning

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

Go to latest
Published: Jul 9, 2026 License: AGPL-3.0 Imports: 22 Imported by: 0

README

# yuri

simple, sweet, and quick library for handling cryptocurrency payments in go

NOTE: this is extremely experimental

currently supports:
[x] Ethereum (+ERC20)
[x] Litecoin
[x] Bitcoin
[x] Monero
[x] Solana (+tokens)

licensed under the [AGPLv3](./LICENSE)
examples [here](./example_instance_test.go) or on `go doc`

Documentation

Overview

yuri is a relatively self contained cryptocurrency invoice library.

we handle creating invoices, polling known cryptocurrencies (via CryptoProvider) users are expected to bring their own Storage implementation

This project is licensed under the GNU AGPLv3 accessible in the License file at the root of the package.

Example
ctx := context.Background()
instance, err := New(Options{
	Hooks: Hooks{
		OnError: func(err error) {
			// if an error happened during Polling/Run itll be given here
			// instead of nuking ur app. kill the run Context if you want.
			// do you.
		},
		OnInvoiceUpdated: func(ctx context.Context, i Invoice) error {
			// an invoice updated

			if i.Paid() {
				// they paid, its confirmed. do thing.
				return nil
			}

			return nil
		},
	},
	Chains: []CryptoProvider{
		NewMonero(JsonRpcClientConfig{
			Host:     "",
			Username: "",
			Password: "",
			// Client: override or uses http.DefaultClient,
		}),
	},

	Pricing: []PriceProvider{
		// these are the same, by default a CachedPriceProvider lives in memory,
		// if you wish to coordinate/cache the pricing between restarts you can wrap
		// [PricingProvider] with Redis etc.
		NewCachedPriceProvider(NewCoinGeckoPriceProvider(nil)),
		NewCachedPriceProviderWithTTL(NewCoinGeckoPriceProvider(nil), 3*time.Minute),
	},
	Storage: &InMemoryStorage{},

	PollEvery:       15 * time.Second,
	MaxPollDuration: 15 * time.Second,
})

if err != nil {
	// you fucked up an option
	return
}

go instance.Run(ctx)

inv, err := instance.NewInvoice(ctx, InvoiceCreate{
	Chain:      Monero,
	AmountFiat: USD.Of(10.50),
})
if err != nil {
	// do something
	return
}

log.Printf("addr = %s owed = %s", inv.Address, inv.AmountOwed.String())

c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-c

Index

Examples

Constants

View Source
const (
	JsonRPCParseError          = -32700
	JsonRPCInvalidRequestError = -32600
	JsonRPCMethodNotFoundError = -32601
	JsonRPCInvalidParamsError  = -32602
	JsonRPCInternalErrorError  = -32603
)
View Source
const DefaultPriceCacheTTL = 3 * time.Minute

Variables

View Source
var (
	FiatCurrencyNotSupportedErr = errors.New("requested currency is not supported by the pricing provider")
	ChainNotSupportedErr        = errors.New("requested chain/token is not supported by the pricing provider")
)
View Source
var (
	USD = Currency{Code: "USD", Decimals: 2}
	EUR = Currency{Code: "EUR", Decimals: 2}
	GBP = Currency{Code: "GBP", Decimals: 2}
	JPY = Currency{Code: "JPY", Decimals: 0}
)

Functions

func NewBNB added in v0.0.5

func NewBNB(rpcConf JsonRpcClientConfig) ethereumLike

NewBNB constructs a new ethereumLike CryptoProvider preconfigured for the BNB chain.

func NewBitcoin

func NewBitcoin(rpcConf JsonRpcClientConfig) bitcoinLike

func NewEthereum

func NewEthereum(rpcConf JsonRpcClientConfig) ethereumLike

NewEthereum constructs a new ethereumLike CryptoProvider preconfigured for the standard Eth chain.

func NewEthereumLike added in v0.0.5

func NewEthereumLike(chain, symbol string, rpcConf JsonRpcClientConfig) ethereumLike

NewEthereumLike constructs a new ethereumLike CryptoProvider for generic EVM compatible chains. For example Eth(base), BNB, Eth(eth).

Chain is the name of the chain, this must be unique. Symbol is the pricing symbol, for example "ETH" for Eth(eth).

func NewLitecoin

func NewLitecoin(rpcConf JsonRpcClientConfig) bitcoinLike

func NewMonero

func NewMonero(rpcConf JsonRpcClientConfig) monero

NewMonero creates a new CryptoProvider for Monero/XMR

This base implementation always uses account index 0 (zero)

func NewSolana added in v0.0.8

func NewSolana(opts SolanaOptions) solanaProvider

TODO: maybe stop people from being dumb and assuming that they don't have to pass in the hooks. something like that. but until this is v1000.00.05 i don't care! RTFM!

func RPCDo

func RPCDo(
	ctx context.Context,
	client JsonRpcClient,
	req JsonRpcRequest,
	out any,
) error

Types

type Chain

type Chain string

Chain is the full name of the chain in lowercase. (e.g. monero)

const BNB Chain = "bnb"
const Bitcoin Chain = "bitcoin"
const Ethereum Chain = "ethereum"
const Litecoin Chain = "litecoin"
const Monero Chain = "monero"
const Solana Chain = "solana"

type CryptoProvider

type CryptoProvider interface {
	// Chain is the full name of the chain in lowercase. (e.g. monero)
	Chain() Chain

	// CreateAddress creates a new subaddress/derived address from the root address
	// provided at init time. Each provider should handle if they want to manually
	// derive it, or request it from something like a JSON-RPC server.
	CreateAddress(context.Context) (string, error)

	// Decimals gets the amount of decimals for the Native currency of this provider
	Decimals() int64

	// Poll gets called with the Invoices with a matching [Chain].
	// It is expected to return the updated values for each invoice.
	//
	// You are expected to return a new [Invoice] with updated balances,
	// or anything else of relevance. You are expected to copy over metadata
	// between the invoices.
	//
	// Please view [Invoice]'s comments if you are implementing this.
	Poll(context.Context, []Invoice) ([]Invoice, error)
}

CryptoProvider standardizes how cryptocurrency providers (e.g. monero) should handle core functionality.

Every method implemented by a CryptoProvider must support being called concurrently.

type Currency

type Currency struct {
	Code     string `json:"code"`
	Decimals int    `json:"decimals"`
}

func (Currency) Of

func (c Currency) Of(amount float64) fiat

Of takes in the amount. For example, yuri.USD.Of(10.50) would mean $10.50 USD

func (Currency) ToMinor

func (c Currency) ToMinor(amount float64) int64

type Hooks

type Hooks struct {
	// If [Storage.UpdateInvoices] fails this method is not called for the Invoice(s)
	OnInvoiceUpdated func(context.Context, Invoice) error
	OnError          func(error)
}

Hooks are lifecycle state to that you don't need to handle lifecycle updates in Storage handlers.

type InMemoryStorage

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

InMemoryStorage implements Storage in process memory, this should be used for exclusively testing, as it does not attempt to persist and heavily abuses mutexes.

func (*InMemoryStorage) GetActiveInvoices

func (i *InMemoryStorage) GetActiveInvoices(_ context.Context, chain Chain) ([]Invoice, error)

GetActiveInvoices implements Storage.

func (*InMemoryStorage) NewInvoice

func (i *InMemoryStorage) NewInvoice(_ context.Context, inv Invoice) error

NewInvoice implements Storage.

func (*InMemoryStorage) UpdateInvoices

func (i *InMemoryStorage) UpdateInvoices(_ context.Context, invoices []Invoice) error

UpdateInvoices implements Storage.

type Instance

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

Instance represents a Yuri invoice instance, which is used to manage [CryptoProvider]s. Instance manages creation of invoices, alongside emitting event updates to you, the end user, using Hooks

func New

func New(options Options) (*Instance, error)

New creates a new Instance and validates the provided Options

func (*Instance) NewInvoice

func (i *Instance) NewInvoice(ctx context.Context, invoiceCreate InvoiceCreate) (Invoice, error)

NewInvoice creates a new invoice with the Instance using the respective CryptoProvider and PriceProvider

func (*Instance) Run

func (i *Instance) Run(ctx context.Context)

Run begins polling every Options.PollEvery and blocks whilst doing so until the context is canceled.

If `Storage.UpdateInvoices` fails at any point we do not emit `Hooks.OnInvoiceUpdated` for the Invoice.

type Invoice

type Invoice struct {
	Chain      Chain    `json:"chain"`
	Address    string   `json:"address"`
	AmountOwed *big.Int `json:"amount_owed"`
	AmountPaid *big.Int `json:"amount_paid"`
	Token      Token    `json:"token"`
	// if the amountPaid amount has not reached the required confirmations
	Pending bool `json:"pending"`

	// user provided metadata, typically an ID to allow for
	// quickly locating and updating the Invoice via [Storage.UpdateInvoice]
	Metadata map[string]any `json:"metadata"`
}

Invoice represents the smallest state an invoice can be. You are required to persist the following: Invoice.Chain, Invoice.Address, [Invoice.AmountOwed, Invoice.AmountPaid, Invoice.Token

func (Invoice) Clone

func (i Invoice) Clone() Invoice

func (Invoice) Paid

func (i Invoice) Paid() bool

Paid determines if an Invoice is fully paid, and accounts for pending funds.

AmountPaid >= AmountOwed, but one or more contributing transfers have not reached the required confirmation threshold.

NOTE: depending on the CryptoProvider's implementation this may vary. Some implementations are naive and assume if there is any pending transaction to the addr that is confirming, even if the addr is >=AmountOwed in balance then the entire balance is pending.

This is behaviour you should expect!

type InvoiceCreate

type InvoiceCreate struct {
	Chain      Chain          `json:"chain"`
	Token      Token          `json:"token"`
	AmountFiat fiat           `json:"amount_fiat"`
	Metadata   map[string]any `json:"metadata"`
}

type JsonRpcClient

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

JsonRpcClient is a small JSON-RPC 2.0 client this client does not support batching.

Impls https://www.simple-is-better.org/json-rpc/transport_http.html

func NewJsonRpcClient

func NewJsonRpcClient(conf JsonRpcClientConfig) JsonRpcClient

func (JsonRpcClient) Do

You generally want to use RPCDo instead for an easier life.

type JsonRpcClientConfig

type JsonRpcClientConfig struct {
	Host     string
	Username string
	Password string

	NonB64BasicAuth bool

	// override the default http client
	Client *http.Client
}

type JsonRpcRequest

type JsonRpcRequest struct {
	// Must be exactly 2.0
	JsonRPC string `json:"jsonrpc"`
	Method  string `json:"method"`
	// NOTE: previously we only supported maps/actual objects,
	// but uhhh.. nope! fuck you. Bitcoin forced my hand
	// because they dont accept named args.
	Params any `json:"params,omitempty"`
	// if ID is omitted this request is a Notification
	Id string `json:"id,omitempty"`
}

type JsonRpcResponse

type JsonRpcResponse struct {
	// Must be exactly 2.0
	JsonRPC string `json:"jsonrpc"`

	// mutually exclusive with Error
	Result json.RawMessage `json:"result"`
	// mutually exclusive with Result
	Error jsonRpcError `json:"error"`

	// can be nullable if there was no id in
	// the request object
	Id string `json:"id,omitempty"`
}

type Options

type Options struct {
	Chains  []CryptoProvider
	Pricing []PriceProvider
	Hooks   Hooks
	Storage Storage
	// How often we should poll for new changes on each provided
	// CryptoProvider. The default is 15 seconds.
	PollEvery time.Duration
	// MaxPollDuration is the maximum amount of time a CryptoProvider
	// is allowed to Poll for. (how long each crypto can Poll for)
	MaxPollDuration time.Duration
}

type PriceProvider

type PriceProvider interface {
	WantsFullChainName() bool

	// Get fetches the price of a cryptocurrency(/token) from a remote
	// pricing provider, for example CoinGecko, in the requested currency.
	//
	// Get MUST ALWAYS be safe to call concurrently.
	//
	// The return value should be in the minor fiat unit, for example
	// cents.
	//
	// If the provider does not support the requested fiat currency
	// then CurrencyNotSupportedErr should be thrown.
	//
	// If the provider does not support the requested chain/token
	// then ChainNotSupportedErr should be thrown.
	Get(ctx context.Context, currency Currency, chain string, token Token) (fiatMinorUnits int64, err error)
}

PriceProvider respresents some place where we can fetch current prices for a cryptocurrency. A PriceProvider MUST ALWAYS be safe to call concurrently.

func NewBareBitcoinPriceProvider

func NewBareBitcoinPriceProvider(client *http.Client) PriceProvider

func NewBitbankPriceProvider

func NewBitbankPriceProvider(client *http.Client) PriceProvider

func NewBitcoinKenyaPriceProvider

func NewBitcoinKenyaPriceProvider(client *http.Client) PriceProvider

func NewBitflyerPriceProvider

func NewBitflyerPriceProvider(client *http.Client) PriceProvider

func NewBitmyntPriceProvider

func NewBitmyntPriceProvider(client *http.Client) PriceProvider

func NewBitnobPriceProvider

func NewBitnobPriceProvider(client *http.Client) PriceProvider

func NewBitpayPriceProvider

func NewBitpayPriceProvider(client *http.Client) PriceProvider

func NewBtcTurkPriceProvider

func NewBtcTurkPriceProvider(client *http.Client) PriceProvider

func NewBudaPriceProvider

func NewBudaPriceProvider(client *http.Client) PriceProvider

func NewByllsPriceProvider

func NewByllsPriceProvider(client *http.Client) PriceProvider

func NewCachedPriceProvider

func NewCachedPriceProvider(inner PriceProvider) PriceProvider

func NewCachedPriceProviderWithTTL

func NewCachedPriceProviderWithTTL(inner PriceProvider, ttl time.Duration) PriceProvider

func NewCoinDCXPriceProvider

func NewCoinDCXPriceProvider(client *http.Client) PriceProvider

func NewCoinGeckoPriceProvider

func NewCoinGeckoPriceProvider(client *http.Client) PriceProvider

func NewCoinmatePriceProvider

func NewCoinmatePriceProvider(client *http.Client) PriceProvider

func NewCryptoMarketPriceProvider

func NewCryptoMarketPriceProvider(client *http.Client) PriceProvider

func NewDesiboardPriceProvider

func NewDesiboardPriceProvider(client *http.Client) PriceProvider

func NewFreeCurrencyRatesPriceProvider

func NewFreeCurrencyRatesPriceProvider(client *http.Client) PriceProvider

func NewHitBTCPriceProvider

func NewHitBTCPriceProvider(client *http.Client) PriceProvider

func NewKrakenPriceProvider

func NewKrakenPriceProvider(client *http.Client) PriceProvider

func NewLunoPriceProvider

func NewLunoPriceProvider(client *http.Client) PriceProvider

func NewNullPriceProvider

func NewNullPriceProvider() PriceProvider

func NewRipioPriceProvider

func NewRipioPriceProvider(client *http.Client) PriceProvider

func NewYadioPriceProvider

func NewYadioPriceProvider(client *http.Client) PriceProvider

type PricingSymbolProvider

type PricingSymbolProvider interface {
	PriceSymbol() string
}

PricingSymbolProvider can be implemented by CryptoProvider implementations that need to expose a different market symbol than their Chain name. This is optional and exists so pricing providers can avoid hardcoded symbol maps.

type SolanaHooks added in v0.0.8

type SolanaHooks struct {
	// OnNewAddress is called when NewAddress is called, you are expected
	// to manage your own storage solution for your wallets.
	// Returning an error from this method results in the address not being generated.
	OnNewAddress func(context.Context, ed25519.PublicKey, ed25519.PrivateKey) error
}

SolanaHooks allows the end user to deal with how they want to manage their custodial wallets

type SolanaOptions added in v0.0.8

type SolanaOptions struct {
	// Hooks directly manage storing the generated wallets, it is on you
	// to store your wallets in a safe place... atleast if you want to get
	// your funds out of the invoice wallets. heh..
	Hooks SolanaHooks
	Rpc   JsonRpcClientConfig
	// contains filtered or unexported fields
}

type Storage

type Storage interface {
	GetActiveInvoices(context.Context, Chain) ([]Invoice, error)
	UpdateInvoices(context.Context, []Invoice) error
	// Please view Invoice to see what you are required to persist.
	NewInvoice(context.Context, Invoice) error
}

Storage represents a user defined storage for storing invoices. All methods on Storage must be safe to call in any goroutine.

type Token

type Token struct {
	Symbol   string `json:"symbol"`
	Contract string `json:"contract"`
	Decimals int64  `json:"decimals"`
}

Token represents a smart contract on a respective chain for example, USDT erc20

var EthereumUSDC Token = Token{
	Symbol:   "USDC",
	Contract: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
	Decimals: 6,
}

EthereumUSDC is USD(Circle) on Eth(eth) not Eth(base)

var EthereumUSDT Token = Token{
	Symbol:   "USDT",
	Contract: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
	Decimals: 6,
}

EthereumUSDT is USD(Tether) on Eth(eth) not Eth(base)

Directories

Path Synopsis
cmd
pricing_test command
yurid command
internal
solana
source: https://github.com/solana-foundation/solana-go/tree/main/base58 copied license: [LICENSE](./LICENSE)
source: https://github.com/solana-foundation/solana-go/tree/main/base58 copied license: [LICENSE](./LICENSE)
solana/base58
source: https://github.com/solana-foundation/solana-go/tree/main/base58 copied license: [LICENSE](./LICENSE)
source: https://github.com/solana-foundation/solana-go/tree/main/base58 copied license: [LICENSE](./LICENSE)

Jump to

Keyboard shortcuts

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