2017-09-12 18:04:45 +02:00
|
|
|
package jsonrpc
|
|
|
|
|
|
|
|
import (
|
2017-09-12 22:38:18 +02:00
|
|
|
"encoding/json"
|
2017-09-19 20:14:52 +02:00
|
|
|
"fmt"
|
2018-03-09 01:27:54 +01:00
|
|
|
"net/http"
|
2017-09-19 20:14:52 +02:00
|
|
|
"reflect"
|
2017-10-04 16:31:03 +02:00
|
|
|
"sort"
|
2017-09-12 18:04:45 +02:00
|
|
|
"strconv"
|
2017-09-19 20:14:52 +02:00
|
|
|
"strings"
|
2017-12-28 18:14:33 +01:00
|
|
|
"time"
|
2017-09-12 18:04:45 +02:00
|
|
|
|
2019-04-03 00:44:30 +02:00
|
|
|
"github.com/fatih/structs"
|
|
|
|
|
2019-01-09 19:05:41 +01:00
|
|
|
"github.com/lbryio/lbry.go/extras/errors"
|
2018-03-09 17:47:38 +01:00
|
|
|
|
2017-09-12 18:04:45 +02:00
|
|
|
"github.com/mitchellh/mapstructure"
|
2017-11-02 16:20:22 +01:00
|
|
|
"github.com/shopspring/decimal"
|
2017-09-13 00:15:26 +02:00
|
|
|
log "github.com/sirupsen/logrus"
|
2017-09-12 18:04:45 +02:00
|
|
|
"github.com/ybbus/jsonrpc"
|
|
|
|
)
|
|
|
|
|
|
|
|
const DefaultPort = 5279
|
|
|
|
|
|
|
|
type Client struct {
|
2018-05-09 00:11:24 +02:00
|
|
|
conn jsonrpc.RPCClient
|
|
|
|
address string
|
2017-09-12 18:04:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func NewClient(address string) *Client {
|
|
|
|
d := Client{}
|
|
|
|
|
|
|
|
if address == "" {
|
|
|
|
address = "http://localhost:" + strconv.Itoa(DefaultPort)
|
|
|
|
}
|
|
|
|
|
2018-05-09 00:11:24 +02:00
|
|
|
d.conn = jsonrpc.NewClient(address)
|
|
|
|
d.address = address
|
2017-09-12 18:04:45 +02:00
|
|
|
|
|
|
|
return &d
|
|
|
|
}
|
|
|
|
|
2017-12-28 18:14:33 +01:00
|
|
|
func NewClientAndWait(address string) *Client {
|
|
|
|
d := NewClient(address)
|
|
|
|
for {
|
2019-01-22 18:48:04 +01:00
|
|
|
_, err := d.AccountBalance(nil)
|
2017-12-28 18:14:33 +01:00
|
|
|
if err == nil {
|
|
|
|
return d
|
|
|
|
}
|
|
|
|
time.Sleep(5 * time.Second)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-18 19:10:23 +01:00
|
|
|
func Decode(data interface{}, targetStruct interface{}) error {
|
2017-09-12 18:04:45 +02:00
|
|
|
config := &mapstructure.DecoderConfig{
|
|
|
|
Metadata: nil,
|
|
|
|
Result: targetStruct,
|
|
|
|
TagName: "json",
|
|
|
|
//WeaklyTypedInput: true,
|
|
|
|
DecodeHook: fixDecodeProto,
|
|
|
|
}
|
|
|
|
|
|
|
|
decoder, err := mapstructure.NewDecoder(config)
|
|
|
|
if err != nil {
|
2017-11-02 16:20:22 +01:00
|
|
|
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:
|
2018-03-09 17:47:38 +01:00
|
|
|
return decimal.Decimal{}, errors.Err("unexpected number type")
|
2017-11-02 16:20:22 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
dec, err := decimal.NewFromString(number)
|
|
|
|
if err != nil {
|
|
|
|
return decimal.Decimal{}, errors.Wrap(err, 0)
|
2017-09-12 18:04:45 +02:00
|
|
|
}
|
|
|
|
|
2017-11-02 16:20:22 +01:00
|
|
|
return dec, nil
|
2017-09-12 18:04:45 +02:00
|
|
|
}
|
|
|
|
|
2017-09-14 21:13:59 +02:00
|
|
|
func debugParams(params map[string]interface{}) string {
|
|
|
|
var s []string
|
|
|
|
for k, v := range params {
|
|
|
|
r := reflect.ValueOf(v)
|
2017-09-16 00:13:13 +02:00
|
|
|
if r.Kind() == reflect.Ptr {
|
|
|
|
if r.IsNil() {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
v = r.Elem().Interface()
|
2017-09-14 21:13:59 +02:00
|
|
|
}
|
|
|
|
s = append(s, fmt.Sprintf("%s=%+v", k, v))
|
|
|
|
}
|
2017-10-04 16:31:03 +02:00
|
|
|
sort.Strings(s)
|
2017-09-14 21:13:59 +02:00
|
|
|
return strings.Join(s, " ")
|
|
|
|
}
|
|
|
|
|
2017-09-12 22:38:18 +02:00
|
|
|
func (d *Client) callNoDecode(command string, params map[string]interface{}) (interface{}, error) {
|
2017-09-14 21:13:59 +02:00
|
|
|
log.Debugln("jsonrpc: " + command + " " + debugParams(params))
|
2018-05-09 00:11:24 +02:00
|
|
|
r, err := d.conn.Call(command, params)
|
2017-09-12 18:04:45 +02:00
|
|
|
if err != nil {
|
2017-11-02 16:20:22 +01:00
|
|
|
return nil, errors.Wrap(err, 0)
|
2017-09-12 18:04:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if r.Error != nil {
|
2018-03-09 17:47:38 +01:00
|
|
|
return nil, errors.Err("Error in daemon: " + r.Error.Message)
|
2017-09-12 18:04:45 +02:00
|
|
|
}
|
|
|
|
|
2017-09-12 22:38:18 +02:00
|
|
|
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
|
|
|
|
}
|
2019-01-18 19:10:23 +01:00
|
|
|
return Decode(result, response)
|
2017-09-12 18:04:45 +02:00
|
|
|
}
|
|
|
|
|
2018-03-09 01:27:54 +01:00
|
|
|
func (d *Client) SetRPCTimeout(timeout time.Duration) {
|
2018-05-09 00:11:24 +02:00
|
|
|
d.conn = jsonrpc.NewClientWithOpts(d.address, &jsonrpc.RPCClientOpts{
|
|
|
|
HTTPClient: &http.Client{Timeout: timeout},
|
|
|
|
})
|
2018-03-09 01:27:54 +01:00
|
|
|
}
|
|
|
|
|
2019-01-17 21:38:45 +01:00
|
|
|
//============================================
|
|
|
|
// NEW SDK
|
|
|
|
//============================================
|
|
|
|
func (d *Client) AccountList() (*AccountListResponse, error) {
|
|
|
|
response := new(AccountListResponse)
|
|
|
|
return response, d.call(response, "account_list", map[string]interface{}{})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *Client) AccountBalance(account *string) (*AccountBalanceResponse, error) {
|
|
|
|
response := new(AccountBalanceResponse)
|
|
|
|
return response, d.call(response, "account_balance", map[string]interface{}{
|
|
|
|
"account_id": account,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *Client) AccountFund(fromAccount string, toAccount string, amount string, outputs uint64) (*AccountFundResponse, error) {
|
|
|
|
response := new(AccountFundResponse)
|
|
|
|
return response, d.call(response, "account_fund", map[string]interface{}{
|
|
|
|
"from_account": fromAccount,
|
|
|
|
"to_account": toAccount,
|
|
|
|
"amount": amount,
|
|
|
|
"outputs": outputs,
|
2019-01-29 21:41:28 +01:00
|
|
|
"broadcast": true,
|
2019-01-17 21:38:45 +01:00
|
|
|
})
|
|
|
|
}
|
2019-01-18 19:10:23 +01:00
|
|
|
|
2019-04-10 23:23:24 +02:00
|
|
|
func (d *Client) AccountCreate(accountName string, singleKey bool) (*AccountCreateResponse, error) {
|
|
|
|
response := new(AccountCreateResponse)
|
|
|
|
return response, d.call(response, "account_create", map[string]interface{}{
|
|
|
|
"account_name": accountName,
|
|
|
|
"single_key": singleKey,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-01-18 19:10:23 +01:00
|
|
|
func (d *Client) AddressUnused(account *string) (*AddressUnusedResponse, error) {
|
|
|
|
response := new(AddressUnusedResponse)
|
|
|
|
return response, d.call(response, "address_unused", map[string]interface{}{
|
|
|
|
"account_id": account,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *Client) ChannelList(account *string, page uint64, pageSize uint64) (*ChannelListResponse, error) {
|
2019-01-22 18:48:04 +01:00
|
|
|
if page == 0 {
|
|
|
|
return nil, errors.Err("pages start from 1")
|
|
|
|
}
|
2019-01-18 19:10:23 +01:00
|
|
|
response := new(ChannelListResponse)
|
|
|
|
return response, d.call(response, "channel_list", map[string]interface{}{
|
|
|
|
"account_id": account,
|
|
|
|
"page": page,
|
|
|
|
"page_size": pageSize,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-04-03 00:44:30 +02:00
|
|
|
type streamType string
|
|
|
|
|
|
|
|
var (
|
|
|
|
StreamTypeVideo = streamType("video")
|
|
|
|
StreamTypeAudio = streamType("audio")
|
|
|
|
StreamTypeImage = streamType("image")
|
|
|
|
)
|
|
|
|
|
|
|
|
type Locations struct {
|
|
|
|
Country *string `json:"country,omitempty"`
|
|
|
|
State *string `json:"state,omitempty"`
|
|
|
|
City *string `json:"city,omitempty"`
|
|
|
|
PostalCode *string `json:"code,omitempty"`
|
|
|
|
Latitude *string `json:"latitude,omitempty"`
|
|
|
|
Longitude *string `json:"longitude,omitempty"`
|
|
|
|
}
|
|
|
|
type ClaimCreateOptions struct {
|
|
|
|
Title string `json:"title"`
|
|
|
|
Description string `json:"description"`
|
|
|
|
Tags []string `json:"tags,omitempty"`
|
|
|
|
Languages []string `json:"language"`
|
|
|
|
Locations *Locations `json:"locations,omitempty"`
|
|
|
|
ThumbnailURL *string `json:"thumbnail_url,omitempty"`
|
|
|
|
AccountID *string `json:"account_id,omitempty"`
|
|
|
|
ClaimAddress *string `json:"claim_address,omitempty"`
|
|
|
|
ChangeAddress *string `json:"change_address,omitempty"`
|
|
|
|
Preview *bool `json:"preview,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type ChannelCreateOptions struct {
|
|
|
|
*ClaimCreateOptions `json:",flatten"`
|
|
|
|
ContactEmail *string `json:"contact_email,omitempty"`
|
|
|
|
HomepageURL *string `json:"homepage_url,omitempty"`
|
|
|
|
CoverURL *string `json:"cover_url,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *Client) ChannelCreate(name string, bid float64, options *ChannelCreateOptions) (*PublishResponse, error) {
|
2019-01-18 19:10:23 +01:00
|
|
|
response := new(PublishResponse)
|
|
|
|
args := struct {
|
2019-04-03 00:44:30 +02:00
|
|
|
Name string `json:"name"`
|
|
|
|
Bid string `json:"bid"`
|
|
|
|
FilePath string `json:"file_path,omitempty"`
|
|
|
|
*ChannelCreateOptions `json:",flatten"`
|
2019-01-18 19:10:23 +01:00
|
|
|
}{
|
2019-04-03 00:44:30 +02:00
|
|
|
Name: name,
|
|
|
|
Bid: fmt.Sprintf("%.6f", bid),
|
|
|
|
ChannelCreateOptions: options,
|
2019-01-18 19:10:23 +01:00
|
|
|
}
|
|
|
|
structs.DefaultTagName = "json"
|
2019-04-03 00:44:30 +02:00
|
|
|
return response, d.call(response, "channel_create", structs.Map(args))
|
2019-01-18 19:10:23 +01:00
|
|
|
}
|
2019-01-18 21:52:57 +01:00
|
|
|
|
2019-04-03 00:44:30 +02:00
|
|
|
type StreamCreateOptions struct {
|
|
|
|
*ClaimCreateOptions `json:",flatten"`
|
|
|
|
Fee *Fee `json:"fee,omitempty"`
|
|
|
|
Author *string `json:"author"`
|
|
|
|
License *string `json:"license"`
|
|
|
|
LicenseURL *string `json:"license_url,omitempty"`
|
|
|
|
StreamType *streamType `json:"stream_type,omitempty"`
|
|
|
|
ReleaseTime *int `json:"release_time,omitempty"`
|
|
|
|
Duration *int `json:"duration,omitempty"`
|
|
|
|
ImageWidth *int `json:"image_width,omitempty"`
|
|
|
|
ImageHeigth *int `json:"image_heigth,omitempty"`
|
|
|
|
VideoWidth *int `json:"video_width,omitempty"`
|
|
|
|
VideoHeight *int `json:"video_height,omitempty"`
|
|
|
|
Preview *string `json:"preview,omitempty"`
|
|
|
|
AllowDuplicateName *bool `json:"allow_duplicate_name,omitempty"`
|
|
|
|
ChannelName *string `json:"channel_name,omitempty"`
|
|
|
|
ChannelID *string `json:"channel_id,omitempty"`
|
|
|
|
ChannelAccountID *string `json:"channel_account_id,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *Client) StreamCreate(name, filePath string, bid float64, options StreamCreateOptions) (*PublishResponse, error) {
|
|
|
|
response := new(PublishResponse)
|
|
|
|
args := struct {
|
|
|
|
Name string `json:"name"`
|
|
|
|
Bid string `json:"bid"`
|
|
|
|
FilePath string `json:"file_path,omitempty"`
|
|
|
|
*StreamCreateOptions `json:",flatten"`
|
|
|
|
}{
|
|
|
|
Name: name,
|
|
|
|
FilePath: filePath,
|
|
|
|
Bid: fmt.Sprintf("%.6f", bid),
|
|
|
|
StreamCreateOptions: &options,
|
|
|
|
}
|
|
|
|
structs.DefaultTagName = "json"
|
|
|
|
return response, d.call(response, "stream_create", structs.Map(args))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *Client) StreamAbandon(txID string, nOut uint64, accountID *string, blocking bool) (*ClaimAbandonResponse, error) {
|
|
|
|
response := new(ClaimAbandonResponse)
|
|
|
|
err := d.call(response, "claim_abandon", map[string]interface{}{
|
|
|
|
"txid": txID,
|
|
|
|
"nout": nOut,
|
|
|
|
"account_id": accountID,
|
2019-01-18 21:52:57 +01:00
|
|
|
})
|
2019-04-03 00:44:30 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
} else if response == nil {
|
|
|
|
return nil, errors.Err("no response")
|
|
|
|
}
|
|
|
|
|
|
|
|
return response, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type StreamUpdateOptions struct {
|
|
|
|
ClearTags *bool `json:"clear_tags,omitempty"`
|
|
|
|
ClearLanguages *bool `json:"clear_languages,omitempty"`
|
|
|
|
ClearLocations *bool `json:"clear_locations,omitempty"`
|
|
|
|
Name *string `json:"name"`
|
|
|
|
FilePath *string `json:"file_path,omitempty"`
|
|
|
|
Bid *string `json:"bid"`
|
|
|
|
*StreamCreateOptions `json:",flatten"`
|
2019-01-18 21:52:57 +01:00
|
|
|
}
|
|
|
|
|
2019-04-03 00:44:30 +02:00
|
|
|
func (d *Client) StreamUpdate(claimID string, options StreamUpdateOptions) (*PublishResponse, error) {
|
|
|
|
response := new(PublishResponse)
|
|
|
|
args := struct {
|
|
|
|
ClaimID string `json:"claim_id"`
|
|
|
|
FilePath string `json:"file_path,omitempty"`
|
|
|
|
Bid string `json:"bid"`
|
|
|
|
*StreamUpdateOptions `json:",flatten"`
|
|
|
|
}{
|
|
|
|
ClaimID: claimID,
|
|
|
|
StreamUpdateOptions: &options,
|
|
|
|
}
|
|
|
|
structs.DefaultTagName = "json"
|
|
|
|
return response, d.call(response, "stream_create", structs.Map(args))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *Client) ChannelAbandon(txID string, nOut uint64, accountID *string, blocking bool) (*ClaimAbandonResponse, error) {
|
2019-01-18 21:52:57 +01:00
|
|
|
response := new(ClaimAbandonResponse)
|
|
|
|
err := d.call(response, "claim_abandon", map[string]interface{}{
|
|
|
|
"txid": txID,
|
|
|
|
"nout": nOut,
|
|
|
|
"account_id": accountID,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
} else if response == nil {
|
|
|
|
return nil, errors.Err("no response")
|
|
|
|
}
|
|
|
|
|
|
|
|
return response, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *Client) AddressList(account *string) (*AddressListResponse, error) {
|
|
|
|
response := new(AddressListResponse)
|
|
|
|
return response, d.call(response, "address_list", map[string]interface{}{
|
|
|
|
"account_id": account,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-04-03 00:44:30 +02:00
|
|
|
func (d *Client) ClaimList(account *string, page uint64, pageSize uint64) (*ClaimListMineResponse, error) {
|
2019-01-22 18:48:04 +01:00
|
|
|
if page == 0 {
|
|
|
|
return nil, errors.Err("pages start from 1")
|
|
|
|
}
|
2019-01-18 21:52:57 +01:00
|
|
|
response := new(ClaimListMineResponse)
|
2019-04-03 00:44:30 +02:00
|
|
|
err := d.call(response, "claim_list", map[string]interface{}{
|
2019-01-18 21:52:57 +01:00
|
|
|
"account_id": account,
|
|
|
|
"page": page,
|
|
|
|
"page_size": pageSize,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
} else if response == nil {
|
|
|
|
return nil, errors.Err("no response")
|
|
|
|
}
|
|
|
|
|
|
|
|
return response, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *Client) Status() (*StatusResponse, error) {
|
|
|
|
response := new(StatusResponse)
|
|
|
|
return response, d.call(response, "status", map[string]interface{}{})
|
|
|
|
}
|
|
|
|
|
2019-01-22 18:48:04 +01:00
|
|
|
func (d *Client) UTXOList(account *string) (*UTXOListResponse, error) {
|
2019-01-18 21:52:57 +01:00
|
|
|
response := new(UTXOListResponse)
|
|
|
|
return response, d.call(response, "utxo_list", map[string]interface{}{
|
|
|
|
"account_id": account,
|
2019-01-22 18:48:04 +01:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *Client) Version() (*VersionResponse, error) {
|
|
|
|
response := new(VersionResponse)
|
|
|
|
return response, d.call(response, "version", map[string]interface{}{})
|
|
|
|
}
|
|
|
|
|
2019-02-22 19:09:13 +01:00
|
|
|
func (d *Client) Resolve(urls string) (*ResolveResponse, error) {
|
2019-01-22 18:48:04 +01:00
|
|
|
response := new(ResolveResponse)
|
|
|
|
return response, d.call(response, "resolve", map[string]interface{}{
|
2019-02-22 19:09:13 +01:00
|
|
|
"urls": urls,
|
2019-01-22 18:48:04 +01:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-04-03 00:44:30 +02:00
|
|
|
/*
|
|
|
|
// use resolve?
|
|
|
|
func (d *Client) NumClaimsInChannel(channelClaimID string) (uint64, error) {
|
2019-01-22 18:48:04 +01:00
|
|
|
response := new(NumClaimsInChannelResponse)
|
2019-04-03 00:44:30 +02:00
|
|
|
err := d.call(response, "claim_search", map[string]interface{}{
|
|
|
|
"channel_id": channelClaimID,
|
2019-01-22 18:48:04 +01:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
} else if response == nil {
|
|
|
|
return 0, errors.Err("no response")
|
|
|
|
}
|
|
|
|
|
|
|
|
channel, ok := (*response)[uri]
|
|
|
|
if !ok {
|
|
|
|
return 0, errors.Err("url not in response")
|
|
|
|
}
|
|
|
|
if channel.Error != nil {
|
|
|
|
if strings.Contains(*channel.Error, "cannot be resolved") {
|
|
|
|
return 0, nil
|
|
|
|
}
|
|
|
|
return 0, errors.Err(*channel.Error)
|
|
|
|
}
|
|
|
|
return *channel.ClaimsInChannel, nil
|
|
|
|
}
|
2019-04-03 00:44:30 +02:00
|
|
|
*/
|
|
|
|
func (d *Client) ClaimSearch(claimName, claimID, txid *string, nout *uint) (*ClaimSearchResponse, error) {
|
|
|
|
response := new(ClaimSearchResponse)
|
|
|
|
return response, d.call(response, "claim_search", map[string]interface{}{
|
2019-01-22 18:48:04 +01:00
|
|
|
"claim_id": claimID,
|
|
|
|
"txid": txid,
|
|
|
|
"nout": nout,
|
2019-04-03 00:44:30 +02:00
|
|
|
"name": claimName,
|
2019-01-18 21:52:57 +01:00
|
|
|
})
|
|
|
|
}
|