package jsonrpc import ( "encoding/json" "fmt" "net/http" "reflect" "sort" "strconv" "strings" "time" "github.com/lbryio/lbry.go/errors" "github.com/mitchellh/mapstructure" "github.com/shopspring/decimal" log "github.com/sirupsen/logrus" "github.com/ybbus/jsonrpc" ) const DefaultPort = 5279 type Client struct { conn *jsonrpc.RPCClient } func NewClient(address string) *Client { d := Client{} if address == "" { address = "http://localhost:" + strconv.Itoa(DefaultPort) } d.conn = jsonrpc.NewRPCClient(address) return &d } func NewClientAndWait(address string) *Client { d := NewClient(address) for { _, err := d.WalletBalance() if err == nil { return d } time.Sleep(5 * time.Second) } } func decode(data interface{}, targetStruct interface{}) error { config := &mapstructure.DecoderConfig{ Metadata: nil, Result: targetStruct, TagName: "json", //WeaklyTypedInput: true, DecodeHook: fixDecodeProto, } decoder, err := mapstructure.NewDecoder(config) if err != nil { return errors.Wrap(err, 0) } err = decoder.Decode(data) if err != nil { return errors.Wrap(err, 0) } return nil } func decodeNumber(data interface{}) (decimal.Decimal, error) { var number string switch d := data.(type) { case json.Number: number = d.String() case string: number = d default: return decimal.Decimal{}, errors.Err("unexpected number type") } dec, err := decimal.NewFromString(number) if err != nil { return decimal.Decimal{}, errors.Wrap(err, 0) } return dec, nil } func debugParams(params map[string]interface{}) string { var s []string for k, v := range params { r := reflect.ValueOf(v) if r.Kind() == reflect.Ptr { if r.IsNil() { continue } v = r.Elem().Interface() } s = append(s, fmt.Sprintf("%s=%+v", k, v)) } sort.Strings(s) return strings.Join(s, " ") } func (d *Client) callNoDecode(command string, params map[string]interface{}) (interface{}, error) { log.Debugln("jsonrpc: " + command + " " + debugParams(params)) r, err := d.conn.CallNamed(command, params) if err != nil { return nil, errors.Wrap(err, 0) } if r.Error != nil { return nil, errors.Err("Error in daemon: " + r.Error.Message) } return r.Result, nil } func (d *Client) call(response interface{}, command string, params map[string]interface{}) error { result, err := d.callNoDecode(command, params) if err != nil { return err } return decode(result, response) } func (d *Client) SetRPCTimeout(timeout time.Duration) { d.conn.SetHTTPClient(&http.Client{Timeout: timeout}) } func (d *Client) Commands() (*CommandsResponse, error) { response := new(CommandsResponse) return response, d.call(response, "commands", map[string]interface{}{}) } func (d *Client) Status() (*StatusResponse, error) { response := new(StatusResponse) return response, d.call(response, "status", map[string]interface{}{}) } func (d *Client) WalletBalance() (*WalletBalanceResponse, error) { rawResponse, err := d.callNoDecode("wallet_balance", map[string]interface{}{}) if err != nil { return nil, err } dec, err := decodeNumber(rawResponse) if err != nil { return nil, err } response := WalletBalanceResponse(dec) return &response, nil } func (d *Client) WalletList() (*WalletListResponse, error) { response := new(WalletListResponse) return response, d.call(response, "wallet_list", map[string]interface{}{}) } func (d *Client) UTXOList() (*UTXOListResponse, error) { response := new(UTXOListResponse) return response, d.call(response, "utxo_list", map[string]interface{}{}) } func (d *Client) Version() (*VersionResponse, error) { response := new(VersionResponse) return response, d.call(response, "version", map[string]interface{}{}) } func (d *Client) Get(url string, filename *string, timeout *uint) (*GetResponse, error) { response := new(GetResponse) return response, d.call(response, "get", map[string]interface{}{ "uri": url, "file_name": filename, "timeout": timeout, }) } func (d *Client) ClaimList(name string) (*ClaimListResponse, error) { response := new(ClaimListResponse) return response, d.call(response, "claim_list", map[string]interface{}{ "name": name, }) } func (d *Client) ClaimShow(claimID *string, txid *string, nout *uint) (*ClaimShowResponse, error) { response := new(ClaimShowResponse) return response, d.call(response, "claim_show", map[string]interface{}{ "claim_id": claimID, "txid": txid, "nout": nout, }) } func (d *Client) PeerList(blobHash string, timeout *uint) (*PeerListResponse, error) { rawResponse, err := d.callNoDecode("peer_list", map[string]interface{}{ "blob_hash": blobHash, "timeout": timeout, }) if err != nil { return nil, err } castResponse, ok := rawResponse.([]interface{}) if !ok { return nil, errors.Err("invalid peer_list response") } peers := []PeerListResponsePeer{} for _, peer := range castResponse { t, ok := peer.([]interface{}) if !ok { return nil, errors.Err("invalid peer_list response") } if len(t) != 3 { return nil, errors.Err("invalid triplet in peer_list response") } ip, ok := t[0].(string) if !ok { return nil, errors.Err("invalid ip in peer_list response") } port, ok := t[1].(json.Number) if !ok { return nil, errors.Err("invalid port in peer_list response") } available, ok := t[2].(bool) if !ok { return nil, errors.Err("invalid is_available in peer_list response") } portNum, err := port.Int64() if err != nil { return nil, errors.Wrap(err, 0) } else if portNum < 0 { return nil, errors.Err("invalid port in peer_list response") } peers = append(peers, PeerListResponsePeer{ IP: ip, Port: uint(portNum), IsAvailable: available, }) } response := PeerListResponse(peers) return &response, nil } func (d *Client) BlobGet(blobHash string, encoding *string, timeout *uint) (*BlobGetResponse, error) { rawResponse, err := d.callNoDecode("blob_get", map[string]interface{}{ "blob_hash": blobHash, "timeout": timeout, "encoding": encoding, }) if err != nil { return nil, err } if _, ok := rawResponse.(string); ok { return nil, nil // blob was downloaded, nothing to return } response := new(BlobGetResponse) return response, decode(rawResponse, response) } func (d *Client) StreamCostEstimate(url string, size *uint64) (*StreamCostEstimateResponse, error) { rawResponse, err := d.callNoDecode("stream_cost_estimate", map[string]interface{}{ "uri": url, "size": size, }) if err != nil { return nil, err } dec, err := decodeNumber(rawResponse) if err != nil { return nil, err } response := StreamCostEstimateResponse(dec) return &response, nil } type FileListOptions struct { SDHash *string StreamHash *string FileName *string ClaimID *string Outpoint *string RowID *string Name *string } func (d *Client) FileList(options FileListOptions) (*FileListResponse, error) { response := new(FileListResponse) return response, d.call(response, "file_list", map[string]interface{}{ "sd_hash": options.SDHash, "stream_hash": options.StreamHash, "file_name": options.FileName, "claim_id": options.ClaimID, "outpoint": options.Outpoint, "rowid": options.RowID, "name": options.Name, }) } func (d *Client) Resolve(url string) (*ResolveResponse, error) { response := new(ResolveResponse) return response, d.call(response, "resolve", map[string]interface{}{ "uri": url, }) } func (d *Client) ChannelNew(name string, amount float64) (*ChannelNewResponse, error) { response := new(ChannelNewResponse) return response, d.call(response, "channel_new", map[string]interface{}{ "channel_name": name, "amount": amount, }) } func (d *Client) ChannelList() (*ChannelListResponse, error) { response := new(ChannelListResponse) return response, d.call(response, "channel_list", map[string]interface{}{}) } type PublishOptions struct { Fee *Fee Title *string Description *string Author *string Language *string License *string LicenseURL *string Thumbnail *string Preview *string NSFW *bool ChannelName *string ChannelID *string ClaimAddress *string ChangeAddress *string } func (d *Client) Publish(name, filePath string, bid float64, options PublishOptions) (*PublishResponse, error) { response := new(PublishResponse) return response, d.call(response, "publish", map[string]interface{}{ "name": name, "file_path": filePath, "bid": bid, "fee": options.Fee, "title": options.Title, "description": options.Description, "author": options.Author, "language": options.Language, "license": options.License, "license_url": options.LicenseURL, "thumbnail": options.Thumbnail, "preview": options.Preview, "nsfw": options.NSFW, "channel_name": options.ChannelName, "channel_id": options.ChannelID, "claim_address": options.ClaimAddress, "change_address": options.ChangeAddress, }) } func (d *Client) BlobAnnounce(blobHash, sdHash, streamHash *string) (*BlobAnnounceResponse, error) { response := new(BlobAnnounceResponse) return response, d.call(response, "blob_announce", map[string]interface{}{ "blob_hash": blobHash, "stream_hash": streamHash, "sd_hash": sdHash, }) } func (d *Client) WalletPrefillAddresses(numAddresses int, amount decimal.Decimal, broadcast bool) (*WalletPrefillAddressesResponse, error) { if numAddresses < 1 { return nil, errors.Err("must create at least 1 address") } response := new(WalletPrefillAddressesResponse) return response, d.call(response, "wallet_prefill_addresses", map[string]interface{}{ "num_addresses": numAddresses, "amount": amount, "no_broadcast": !broadcast, }) } func (d *Client) WalletNewAddress() (*WalletNewAddressResponse, error) { rawResponse, err := d.callNoDecode("wallet_new_address", map[string]interface{}{}) if err != nil { return nil, err } address, ok := rawResponse.(string) if !ok { return nil, errors.Err("unexpected response") } response := WalletNewAddressResponse(address) return &response, nil } func (d *Client) WalletUnusedAddress() (*WalletUnusedAddressResponse, error) { rawResponse, err := d.callNoDecode("wallet_unused_address", map[string]interface{}{}) if err != nil { return nil, err } address, ok := rawResponse.(string) if !ok { return nil, errors.Err("unexpected response") } response := WalletUnusedAddressResponse(address) return &response, nil } func (d *Client) NumClaimsInChannel(url string) (uint64, error) { response := new(NumClaimsInChannelResponse) err := d.call(response, "claim_list_by_channel", map[string]interface{}{ "uri": url, }) if err != nil { return 0, err } else if response == nil { return 0, errors.Err("no response") } channel, ok := (*response)[url] if !ok { return 0, errors.Err("url not in response") } if channel.Error != "" { if strings.Contains(channel.Error, "cannot be resolved") { return 0, nil } return 0, errors.Err(channel.Error) } return channel.ClaimsInChannel, nil }