Merge pull request #73 from lbryio/feature/wallet

Add wallet commands to JSON-RPC client
This commit is contained in:
sayplastic 2019-10-08 13:08:57 +07:00 committed by GitHub
commit 9c278d131d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 245 additions and 30 deletions

View file

@ -137,11 +137,17 @@ func (d *Client) SetRPCTimeout(timeout time.Duration) {
//============================================ //============================================
// NEW SDK // NEW SDK
//============================================ //============================================
func (d *Client) AccountList() (*AccountListResponse, error) { func (d *Client) AccountList() (*AccountListResponse, error) {
response := new(AccountListResponse) response := new(AccountListResponse)
return response, d.call(response, "account_list", map[string]interface{}{}) return response, d.call(response, "account_list", map[string]interface{}{})
} }
func (d *Client) AccountListForWallet(walletID string) (*AccountListResponse, error) {
response := new(AccountListResponse)
return response, d.call(response, "account_list", map[string]interface{}{"wallet_id": walletID})
}
func (d *Client) SingleAccountList(accountID string) (*Account, error) { func (d *Client) SingleAccountList(accountID string) (*Account, error) {
response := new(Account) response := new(Account)
return response, d.call(response, "account_list", map[string]interface{}{"account_id": accountID}) return response, d.call(response, "account_list", map[string]interface{}{"account_id": accountID})
@ -211,7 +217,7 @@ func (d *Client) AddressUnused(account *string) (*AddressUnusedResponse, error)
}) })
} }
func (d *Client) ChannelList(account *string, page uint64, pageSize uint64) (*ChannelListResponse, error) { func (d *Client) ChannelList(account *string, page uint64, pageSize uint64, wid *string) (*ChannelListResponse, error) {
if page == 0 { if page == 0 {
return nil, errors.Err("pages start from 1") return nil, errors.Err("pages start from 1")
} }
@ -221,6 +227,7 @@ func (d *Client) ChannelList(account *string, page uint64, pageSize uint64) (*Ch
"page": page, "page": page,
"page_size": pageSize, "page_size": pageSize,
"include_protobuf": true, "include_protobuf": true,
"wallet_id": wid,
}) })
} }
@ -521,6 +528,14 @@ func (d *Client) ChannelExport(channelClaimID string, channelName, accountID *st
}) })
} }
func (d *Client) ChannelImport(key string, walletID *string) (*ChannelImportResponse, error) {
response := new(ChannelImportResponse)
return response, d.call(response, "channel_import", map[string]interface{}{
"channel_data": key,
"wallet_id": walletID,
})
}
func (d *Client) SupportList(accountID *string, page uint64, pageSize uint64) (*SupportListResponse, error) { func (d *Client) SupportList(accountID *string, page uint64, pageSize uint64) (*SupportListResponse, error) {
response := new(SupportListResponse) response := new(SupportListResponse)
return response, d.call(response, "support_list", map[string]interface{}{ return response, d.call(response, "support_list", map[string]interface{}{
@ -598,3 +613,39 @@ func (d *Client) AccountAdd(accountName string, seed *string, privateKey *string
structs.DefaultTagName = "json" structs.DefaultTagName = "json"
return response, d.call(response, "account_add", structs.Map(args)) return response, d.call(response, "account_add", structs.Map(args))
} }
type WalletCreateOpts struct {
ID string `json:"wallet_id"`
SkipOnStartup bool `json:"skip_on_startup,omitempty"`
CreateAccount bool `json:"create_account,omitempty"`
SingleKey bool `json:"single_key,omitempty"`
}
func (d *Client) WalletCreate(id string, opts *WalletCreateOpts) (*Wallet, error) {
response := new(Wallet)
if opts == nil {
opts = &WalletCreateOpts{}
}
opts.ID = id
structs.DefaultTagName = "json"
return response, d.call(response, "wallet_create", structs.Map(opts))
}
func (d *Client) WalletAdd(id string) (*Wallet, error) {
response := new(Wallet)
return response, d.call(response, "wallet_add", map[string]interface{}{"wallet_id": id})
}
func (d *Client) WalletList(id string) (*WalletList, error) {
response := new(WalletList)
params := map[string]interface{}{}
if id != "" {
params["wallet_id"] = id
}
return response, d.call(response, "wallet_list", params)
}
func (d *Client) WalletRemove(id string) (*Wallet, error) {
response := new(Wallet)
return response, d.call(response, "wallet_remove", map[string]interface{}{"wallet_id": id})
}

View file

@ -3,6 +3,7 @@ package jsonrpc
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"math/rand"
"os" "os"
"strconv" "strconv"
"strings" "strings"
@ -21,6 +22,12 @@ func prettyPrint(i interface{}) {
fmt.Println(string(s)) fmt.Println(string(s))
} }
func TestMain(m *testing.M) {
rand.Seed(time.Now().UnixNano())
code := m.Run()
os.Exit(code)
}
func TestClient_AccountFund(t *testing.T) { func TestClient_AccountFund(t *testing.T) {
d := NewClient("") d := NewClient("")
accounts, err := d.AccountList() accounts, err := d.AccountList()
@ -59,17 +66,20 @@ func TestClient_AccountList(t *testing.T) {
func TestClient_SingleAccountList(t *testing.T) { func TestClient_SingleAccountList(t *testing.T) {
d := NewClient("") d := NewClient("")
createdAccount, err := d.AccountCreate("test"+fmt.Sprintf("%d", time.Now().Unix())+"@lbry.com", false) name := "test" + fmt.Sprintf("%d", rand.Int()) + "@lbry.com"
createdAccount, err := d.AccountCreate(name, false)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
return
} }
account, err := d.SingleAccountList(createdAccount.ID) account, err := d.SingleAccountList(createdAccount.ID)
prettyPrint(*createdAccount)
prettyPrint(*account)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
return
} }
prettyPrint(*account) if account.Name != name {
t.Fatalf("account name mismatch: %v != %v", account.Name, name)
}
} }
func TestClient_AccountBalance(t *testing.T) { func TestClient_AccountBalance(t *testing.T) {
@ -94,7 +104,7 @@ func TestClient_AddressUnused(t *testing.T) {
func TestClient_ChannelList(t *testing.T) { func TestClient_ChannelList(t *testing.T) {
d := NewClient("") d := NewClient("")
got, err := d.ChannelList(nil, 1, 50) got, err := d.ChannelList(nil, 1, 50, nil)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
return return
@ -433,7 +443,7 @@ func TestClient_AccountSet(t *testing.T) {
func TestClient_AccountCreate(t *testing.T) { func TestClient_AccountCreate(t *testing.T) {
d := NewClient("") d := NewClient("")
name := "test" + fmt.Sprintf("%d", time.Now().Unix()) + "@lbry.com" name := "lbry#user#id:" + fmt.Sprintf("%d", rand.Int())
account, err := d.AccountCreate(name, false) account, err := d.AccountCreate(name, false)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -468,7 +478,8 @@ func TestClient_AccountAdd(t *testing.T) {
func TestClient_AccountRemove(t *testing.T) { func TestClient_AccountRemove(t *testing.T) {
d := NewClient("") d := NewClient("")
createdAccount, err := d.AccountCreate("test"+fmt.Sprintf("%d", time.Now().Unix())+"@lbry.com", false) name := "lbry#user#id:" + fmt.Sprintf("%d", rand.Int())
createdAccount, err := d.AccountCreate(name, false)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
return return
@ -488,9 +499,7 @@ func TestClient_AccountRemove(t *testing.T) {
prettyPrint(*removedAccount) prettyPrint(*removedAccount)
return return
} }
t.Fatal(err)
t.Error(err)
return
} }
t.Error("account was not removed") t.Error("account was not removed")
prettyPrint(*account) prettyPrint(*account)
@ -507,3 +516,160 @@ func TestClient_ChannelExport(t *testing.T) {
} }
t.Log("Export:", *response) t.Log("Export:", *response)
} }
func TestClient_ChannelImport(t *testing.T) {
d := NewClient("")
// A channel created just for automated testing purposes
channelName := "@LbryAutomatedTestChannel"
channelkey := "7943FWPBHZES4dUcMXSpDYwoM5a2tsyJT1R8V54QoUhekGcqmeH3hbzDXoLLQ8" +
"oKkfb99PgGK5efrZeYqaxg4X5XRJMJ6gKC8hqKcnwhYkmKDXmoBDNgd2ccZ9jhP8z" +
"HG3NJorAN9Hh4XMyBc5goBLZYYvC9MYvBmT3Fcteb5saqMvmQxFURv74NqXLQZC1t" +
"p6iRZKfTj77Pd5gsBsCYAbVmCqzbm5m1hHkUmfFEZVGcQNTYCDwZn543xSMYvSPnJ" +
"zt8tRYCJWaPdj713uENZZMo3gxuAMb1NwSnx8tbwETp7WPkpFLL6HZ9jKpB8BURHM" +
"F1RFD1PRyqbC6YezPyPQ2oninKKHdBduvXZG5KF2G2Q3ixsuE2ntifBBo1f5PotRk" +
"UanXKEafWxvXAayJjpsmZ4bFt7n6Xg4438WZXBiZKCPobLJAiHfe72n618kE6PCNU" +
"77cyU5Rk8J3CuY6QzZPzwuiXz2GLfkUMCYd9jGT6g53XbE6SwCsmGnd9NJkBAaJf5" +
"1FAYRURrhHnp79PAoHftEWtZEuU8MCPMdSRjzxYMRS4ScUzg5viDMTAkE8frsfCVZ" +
"hxsFwGUyNNno8eiqrrYmpbJGEwwK3S4437JboAUEFPdMNn8zNQWZcLLVrK9KyQeKM" +
"XpKkf4zJV6sZJ7gBMpzvPL18ULEgXTy7VsNBKmsfC1rM4WVG9ri1UixEcLDS79foC" +
"Jb3FnSr1T4MRKESeN3W"
response, err := d.ChannelImport(channelkey, nil)
if err != nil {
t.Error(err)
}
channels, err := d.ChannelList(nil, 1, 50, nil)
seen := false
for _, c := range channels.Items {
if c.Name == channelName {
seen = true
}
}
if !seen {
t.Error("couldn't find imported channel")
}
t.Log("Response:", *response)
}
func TestClient_ChannelImportWithWalletID(t *testing.T) {
d := NewClient("")
id := "lbry#wallet#id:" + fmt.Sprintf("%d", rand.Int())
wallet, err := d.WalletCreate(id, nil)
// A channel created just for automated testing purposes
channelName := "@LbryAutomatedTestChannel"
channelkey := "7943FWPBHZES4dUcMXSpDYwoM5a2tsyJT1R8V54QoUhekGcqmeH3hbzDXoLLQ8" +
"oKkfb99PgGK5efrZeYqaxg4X5XRJMJ6gKC8hqKcnwhYkmKDXmoBDNgd2ccZ9jhP8z" +
"HG3NJorAN9Hh4XMyBc5goBLZYYvC9MYvBmT3Fcteb5saqMvmQxFURv74NqXLQZC1t" +
"p6iRZKfTj77Pd5gsBsCYAbVmCqzbm5m1hHkUmfFEZVGcQNTYCDwZn543xSMYvSPnJ" +
"zt8tRYCJWaPdj713uENZZMo3gxuAMb1NwSnx8tbwETp7WPkpFLL6HZ9jKpB8BURHM" +
"F1RFD1PRyqbC6YezPyPQ2oninKKHdBduvXZG5KF2G2Q3ixsuE2ntifBBo1f5PotRk" +
"UanXKEafWxvXAayJjpsmZ4bFt7n6Xg4438WZXBiZKCPobLJAiHfe72n618kE6PCNU" +
"77cyU5Rk8J3CuY6QzZPzwuiXz2GLfkUMCYd9jGT6g53XbE6SwCsmGnd9NJkBAaJf5" +
"1FAYRURrhHnp79PAoHftEWtZEuU8MCPMdSRjzxYMRS4ScUzg5viDMTAkE8frsfCVZ" +
"hxsFwGUyNNno8eiqrrYmpbJGEwwK3S4437JboAUEFPdMNn8zNQWZcLLVrK9KyQeKM" +
"XpKkf4zJV6sZJ7gBMpzvPL18ULEgXTy7VsNBKmsfC1rM4WVG9ri1UixEcLDS79foC" +
"Jb3FnSr1T4MRKESeN3W"
response, err := d.ChannelImport(channelkey, &wallet.ID)
if err != nil {
t.Error(err)
}
channels, err := d.ChannelList(nil, 1, 50, &wallet.ID)
seen := false
for _, c := range channels.Items {
if c.Name == channelName {
seen = true
}
}
if !seen {
t.Error("couldn't find imported channel")
}
t.Log("Response:", *response)
}
func TestClient_WalletCreate(t *testing.T) {
d := NewClient("")
id := "lbry#wallet#id:" + fmt.Sprintf("%d", rand.Int())
wallet, err := d.WalletCreate(id, nil)
if err != nil {
t.Fatal(err)
}
if wallet.ID != id {
prettyPrint(*wallet)
t.Fatalf("wallet ID mismatch, expected %q, got %q", id, wallet.Name)
}
}
func TestClient_WalletCreateWithOpts(t *testing.T) {
d := NewClient("")
id := "lbry#wallet#id:" + fmt.Sprintf("%d", rand.Int())
wallet, err := d.WalletCreate(id, &WalletCreateOpts{CreateAccount: true, SingleKey: true})
if err != nil {
t.Fatal(err)
}
accounts, err := d.AccountListForWallet(id)
if err != nil {
t.Fatal(err)
}
prettyPrint(wallet)
prettyPrint(accounts)
if accounts.LBCMainnet[0].Name == "" {
t.Fatalf("account name is empty")
}
}
func TestClient_WalletList(t *testing.T) {
d := NewClient("")
id := "lbry#wallet#id:" + fmt.Sprintf("%d", rand.Int())
wList, err := d.WalletList(id)
if err == nil {
t.Fatalf("wallet %v was unexpectedly found", id)
}
if err.Error() != fmt.Sprintf("Error in daemon: Couldn't find wallet: %v.", id) {
t.Fatal(err)
}
_, err = d.WalletCreate(id, &WalletCreateOpts{CreateAccount: true, SingleKey: true})
if err != nil {
t.Fatal(err)
}
wList, err = d.WalletList(id)
if err != nil {
t.Fatal(err)
}
if len(*wList) < 1 {
t.Fatal("wallet list is empty")
}
if (*wList)[0].ID != id {
t.Fatalf("wallet ID mismatch, expected %q, got %q", id, (*wList)[0].ID)
}
}
func TestClient_WalletRemoveWalletAdd(t *testing.T) {
d := NewClient("")
id := "lbry#wallet#id:" + fmt.Sprintf("%d", rand.Int())
wallet, err := d.WalletCreate(id, nil)
if err != nil {
t.Fatal(err)
}
_, err = d.WalletRemove(id)
if err != nil {
t.Fatal(err)
}
addedWallet, err := d.WalletAdd(id)
if err != nil {
t.Fatal(err)
}
if addedWallet.ID != wallet.ID {
prettyPrint(*addedWallet)
t.Fatalf("wallet ID mismatch, expected %q, got %q", wallet.ID, addedWallet.Name)
}
}

View file

@ -271,7 +271,9 @@ type AddressListResponse []struct {
Pubkey string `json:"pubkey"` Pubkey string `json:"pubkey"`
UsedTimes uint64 `json:"used_times"` UsedTimes uint64 `json:"used_times"`
} }
type ChannelExportResponse string type ChannelExportResponse string
type ChannelImportResponse string
type ChannelListResponse struct { type ChannelListResponse struct {
Items []Transaction `json:"items"` Items []Transaction `json:"items"`
@ -538,3 +540,10 @@ type NumClaimsInChannelResponse map[string]struct {
} }
type ClaimShowResponse *Claim type ClaimShowResponse *Claim
type Wallet struct {
ID string `json:"id"`
Name string `json:"name"`
}
type WalletList []Wallet

23
go.mod
View file

@ -1,50 +1,35 @@
module github.com/lbryio/lbry.go module github.com/lbryio/lbry.go/v2
require ( require (
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf // indirect
github.com/btcsuite/btcd v0.0.0-20190213025234-306aecffea32 github.com/btcsuite/btcd v0.0.0-20190213025234-306aecffea32
github.com/btcsuite/btcutil v0.0.0-20190207003914-4c204d697803 github.com/btcsuite/btcutil v0.0.0-20190207003914-4c204d697803
github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.1
github.com/fatih/structs v1.1.0 github.com/fatih/structs v1.1.0
github.com/go-errors/errors v1.0.1 github.com/go-errors/errors v1.0.1
github.com/go-ini/ini v1.38.2 github.com/go-ini/ini v1.38.2
github.com/go-ozzo/ozzo-validation v3.5.0+incompatible // indirect
github.com/golang/protobuf v1.3.0 github.com/golang/protobuf v1.3.0
github.com/google/go-cmp v0.2.0 // indirect
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/mux v1.6.2 github.com/gorilla/mux v1.6.2
github.com/gorilla/rpc v1.1.0 github.com/gorilla/rpc v1.1.0
github.com/gorilla/websocket v1.2.0 // indirect github.com/lbryio/lbry.go v1.1.2
github.com/jtolds/gls v4.2.1+incompatible // indirect
github.com/lbryio/lbryschema.go v0.0.0-20190602173230-6d2f69a36f46 github.com/lbryio/lbryschema.go v0.0.0-20190602173230-6d2f69a36f46
github.com/lbryio/ozzo-validation v0.0.0-20170323141101-d1008ad1fd04 github.com/lbryio/ozzo-validation v0.0.0-20170323141101-d1008ad1fd04
github.com/lbryio/types v0.0.0-20190422033210-321fb2abda9c github.com/lbryio/types v0.0.0-20190422033210-321fb2abda9c
github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5 // indirect
github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6 // indirect
github.com/lyoshenka/bencode v0.0.0-20180323155644-b7abd7672df5 github.com/lyoshenka/bencode v0.0.0-20180323155644-b7abd7672df5
github.com/mitchellh/mapstructure v0.0.0-20180511142126-bb74f1db0675 github.com/mitchellh/mapstructure v0.0.0-20180511142126-bb74f1db0675
github.com/nlopes/slack v0.5.0 github.com/nlopes/slack v0.5.0
github.com/pkg/errors v0.8.1 // indirect
github.com/sebdah/goldie v0.0.0-20180424091453-8784dd1ab561 github.com/sebdah/goldie v0.0.0-20180424091453-8784dd1ab561
github.com/sergi/go-diff v1.0.0 github.com/sergi/go-diff v1.0.0
github.com/shopspring/decimal v0.0.0-20180607144847-19e3cb6c2930 github.com/shopspring/decimal v0.0.0-20180607144847-19e3cb6c2930
github.com/sirupsen/logrus v1.2.0 github.com/sirupsen/logrus v1.2.0
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect
github.com/spf13/cast v1.2.0 github.com/spf13/cast v1.2.0
github.com/stretchr/testify v1.3.0 github.com/stretchr/testify v1.3.0
github.com/uber-go/atomic v1.3.2 github.com/uber-go/atomic v1.3.2
github.com/ybbus/jsonrpc v0.0.0-20180411222309-2a548b7d822d github.com/ybbus/jsonrpc v0.0.0-20180411222309-2a548b7d822d
go.uber.org/atomic v1.3.2 // indirect
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a golang.org/x/net v0.0.0-20181114220301-adae6a3d119a
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect
golang.org/x/sys v0.0.0-20190520201301-c432e742b0af // indirect
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c golang.org/x/time v0.0.0-20181108054448-85acf8d2951c
google.golang.org/genproto v0.0.0-20181004005441-af9cb2a35e7f // indirect
google.golang.org/grpc v1.17.0 google.golang.org/grpc v1.17.0
gopkg.in/ini.v1 v1.41.0 // indirect
gopkg.in/nullbio/null.v6 v6.0.0-20161116030900-40264a2e6b79 gopkg.in/nullbio/null.v6 v6.0.0-20161116030900-40264a2e6b79
gotest.tools v2.2.0+incompatible // indirect
) )
go 1.12

4
go.sum
View file

@ -66,6 +66,10 @@ github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 h1:FOOIBWrEkLgmlgGfM
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/lbryio/errors.go v0.0.0-20180223142025-ad03d3cc6a5c/go.mod h1:muH7wpUqE8hRA3OrYYosw9+Sl681BF9cwcjzE+OCNK8=
github.com/lbryio/lbry.go v1.1.2 h1:Dyxc+glT/rVWJwHfIf7vjlPYYbjzrQz5ARmJd5Hp69c=
github.com/lbryio/lbry.go v1.1.2/go.mod h1:JtyI30bU51rm0LZ/po3mQuzf++14OWb6kR/6mMRAmKU=
github.com/lbryio/lbryschema.go v0.0.0-20190428231007-c54836bca002/go.mod h1:dAzPCBj3CKKWBGYBZxK6tKBP5SCgY2tqd9SnQd/OyKo=
github.com/lbryio/lbryschema.go v0.0.0-20190602173230-6d2f69a36f46 h1:LemfR+rMxhf7nnOrzy2HqS7Me7SZ5gEwOcNFzKC8ySQ= github.com/lbryio/lbryschema.go v0.0.0-20190602173230-6d2f69a36f46 h1:LemfR+rMxhf7nnOrzy2HqS7Me7SZ5gEwOcNFzKC8ySQ=
github.com/lbryio/lbryschema.go v0.0.0-20190602173230-6d2f69a36f46/go.mod h1:dAzPCBj3CKKWBGYBZxK6tKBP5SCgY2tqd9SnQd/OyKo= github.com/lbryio/lbryschema.go v0.0.0-20190602173230-6d2f69a36f46/go.mod h1:dAzPCBj3CKKWBGYBZxK6tKBP5SCgY2tqd9SnQd/OyKo=
github.com/lbryio/ozzo-validation v0.0.0-20170323141101-d1008ad1fd04 h1:Nze+C2HbeKvhjI/kVn+9Poj/UuEW5sOQxcsxqO7L3GI= github.com/lbryio/ozzo-validation v0.0.0-20170323141101-d1008ad1fd04 h1:Nze+C2HbeKvhjI/kVn+9Poj/UuEW5sOQxcsxqO7L3GI=