Building File Management Tool with Greenfield SDK¶
Several Chain API libraries are available. These libraries manage the low-level logic of connecting to Greenfield node, making requests, and handing the responses. * go-sdk * js-sdk
In this tutorial we’ll use the go-SDK library to interact with testnet.
Prerequisites¶
Before getting started, you should be familiar with: * Greenfield basics * Greenfield command line examples
Also, make sure you have the following dependencies installed with the latest version: * Go version above 1.20
Go-SDK Features¶
- basic.go includes the basic functions to fetch the blockchain info.
- storage.go includes the most storage functions such as creating a bucket, uploading files, downloading files, heading, and deleting resources.
- group.go includes group-related functions such as creating a group and updating group member.
- payment.go includes the payment-related functions to a management payment account.
- crosschain.go includes the cross-chain-related functions to transfer or mirror resources to BSC.
Setup¶
Create a Go Project¶
Let’s set up a Go project with the necessary dependencies.
Init
$ mkdir ~/hellogreenfield
$ cd ~/hellogreenfield
$ go mod init hellogreenfield
Add SDK Dependencies
$ go get github.com/bnb-chain/greenfield-go-sdk
Edit go.mod to replace dependencies
replace (
cosmossdk.io/api => github.com/bnb-chain/greenfield-cosmos-sdk/api v0.0.0-20230425074444-eb5869b05fe9
cosmossdk.io/math => github.com/bnb-chain/greenfield-cosmos-sdk/math v0.0.0-20230425074444-eb5869b05fe9
github.com/cometbft/cometbft => github.com/bnb-chain/greenfield-cometbft v0.0.2
github.com/cometbft/cometbft-db => github.com/bnb-chain/greenfield-cometbft-db v0.8.1-alpha.1
github.com/cosmos/cosmos-sdk => github.com/bnb-chain/greenfield-cosmos-sdk v0.2.3
github.com/cosmos/iavl => github.com/bnb-chain/greenfield-iavl v0.20.1-alpha.1
github.com/syndtr/goleveldb => github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7
)
Install dependensies
go mod tidy
Test a simple function¶
Now we’re ready to connect to Greenfield testnet and interact with the storage provider APIs. Let’s write a simple script to query the Greenfield version to verify if everything works as expected.
Create a main.go
file in your project and add the following code.
package main
import (
"context"
"log"
"github.com/bnb-chain/greenfield-go-sdk/client"
"github.com/bnb-chain/greenfield-go-sdk/types"
)
const (
rpcAddr = "https://greenfield-chain.bnbchain.org:443"
chainId = "greenfield_1017-1"
/*testnet
rpcAddr = "https://gnfd-testnet-fullnode-tendermint-us.bnbchain.org:443"
chainId = "greenfield_5600-1"
*/
privateKey = ""
)
func main() {
account, err := types.NewAccountFromPrivateKey("test", privateKey)
if err != nil {
log.Fatalf("New account from private key error, %v", err)
}
cli, err := client.New(chainId, rpcAddr, client.Option{DefaultAccount: account})
if err != nil {
log.Fatalf("unable to new greenfield client, %v", err)
}
ctx := context.Background()
nodeInfo, versionInfo, err := cli.GetNodeInfo(ctx)
if err != nil {
log.Fatalf("unable to get node info, %v", err)
}
log.Printf("nodeInfo moniker: %s, go version: %s", nodeInfo.Moniker, versionInfo.GoVersion)
latestBlock, err := cli.GetLatestBlock(ctx)
if err != nil {
log.Fatalf("unable to get latest block, %v", err)
}
log.Printf("latestBlock header: %s", latestBlock.Header)
heightBefore := latestBlock.Header.Height
log.Printf("Wait for block height: %d", heightBefore)
err = cli.WaitForBlockHeight(ctx, heightBefore+10)
if err != nil {
log.Fatalf("unable to wait for block height, %v", err)
}
height, err := cli.GetLatestBlockHeight(ctx)
if err != nil {
log.Fatalf("unable to get latest block height, %v", err)
}
log.Printf("Current block height: %d", height)
}
Run the following command in your project directory:
go run main.go
2023/09/12 22:18:10 nodeInfo moniker: fullnode, go version: go version go1.20.7 linux/amd64
2023/09/12 22:18:10 latestBlock header: {{%!s(uint64=11) %!s(uint64=0)} greenfield_5600-1 %!s(int64=401149) 2023-09-13 04:18:05.661693468 +0000 UTC
{
"header": {
"version": {
"block": "11",
"app": "0"
},
"chain_id": "greenfield_5600-1",
"height": "401149",
"time": "2023-09-13T04:18:05.661693468Z",
"last_block_id": {
"hash": "KenBGYDrtA7Bnyy6j3R3d16GWuHnIl5gJW0J3kmM4r8=",
"part_set_header": {
"total": 1,
"hash": "W6nmeVJEhHinvI4I6HBsU/A87Zma8DVVvddBATJdctE="
}
},
"last_commit_hash": "/G92Jzr8fPpqKY89F3xa3dytOF8a2HLvqCrccm9scXM=",
"data_hash": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
"validators_hash": "FykCd/548F1J28ssZr71B1805hzxENaQvexsW/Dxo3E=",
"next_validators_hash": "FykCd/548F1J28ssZr71B1805hzxENaQvexsW/Dxo3E=",
"consensus_hash": "FgA8CM0pWCco2OYq8pA9tuklVX8bmHmMV2Ssdj31W4E=",
"app_hash": "wv+XqXhJBQPYpat/Obaj00u86KfJ8le4LIIFFAgqVmA=",
"last_results_hash": "f6XeDeH8QasoTSGpSJL0r2WGE4MlrXOVt0cE3bIQE8I=",
"evidence_hash": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
"proposer_address": "KhlQ9bz1O8iaWZnqKe36m3IpcP4=",
"randao_mix": "/6zQmCJztTeqZIRHe/pXxhgSbfwDLE85awoa4c8sShUUwGGLqFyshMag63MTB7JC2fAsUqPg1ryALY+uQNZ3Bw=="
}
}
2023/09/12 22:18:10 Wait for block height: 401149
2023/09/12 22:18:34 Current block height: 401159
Get Chain Data¶
In the previous step, we created a main.go
file to demonstrate the basic steps to connect to the node and initialize a Client
to query chain data. Next, let’s use some more functions.
Get current chain head:
We can add the following code in main.go to query current head of the chain.
blockByHeight, err := cli.GetBlockByHeight(ctx, height)
if err != nil {
log.Fatalf("unable to get block by height, %v", err)
}
log.Printf("Current block height: %d", blockByHeight.Header)
Get Address balance¶
With a given greenfield wallet address, you can query its balance by calling GetAccountBalance
function.
balance, err := cli.GetAccountBalance(ctx, account.GetAddress().String())
if err != nil {
log.Fatalf("unable to get balance, %v", err)
}
log.Printf("%s Current balance: %s", account.GetAddress().String(), balance.String())
Apart from the basic data queries shown above, there are many more features. Please see the JSON-RPC API Reference for all Greenfield API definitions.
Manage Wallet¶
Greenfield wallets hold addresses that you can use to manage objects, sign transactions, and pay for gas fees. In this section, we will demonstrate different ways to manage your wallet.
1. First, let’s make sure your connected node is running and the wallet address contains some testnet BNB.
2. Create a new file called account.go
in the same project as earlier. This is where we’ll write all out wallet-related code.
3. In account.go
import modules and initialize your private key or mnemonic phrase.
//import mnemonic
account, err := types.NewAccountFromMnemonic("test", mnemonic)
//import private key
account, err := types.NewAccountFromPrivateKey("test", privateKey)
Let’s create a second wallet address so we can test transfers. The new address will be created locally and start with 0 token balance:
account2, _, err := types.NewAccount("test2")
Now, let’s try to transfer tBNB to this new address. Under the hood, this will create a transaction to transfer tBNB from fromAddress to toAddress, sign the transaction using SDK, and send the signed transaction to the Greenfield node.
transferTxHash, err := cli.Transfer(ctx, account2.GetAddress().String(), math.NewIntFromUint64(10000000000), types2.TxOption{})
if err != nil {
log.Fatalf("unable to send, %v", err)
}
log.Printf("Transfer response: %s", transferTxHash)
waitForTx, err := cli.WaitForTx(ctx, transferTxHash)
log.Printf("Wair for tx: %s", waitForTx.TxResult.String())
balance, err = cli.GetAccountBalance(ctx, account2.GetAddress().String())
```
Run the code to test the transfer of tBNB:
```sh
go run account.go
This will output something like:
2023/09/07 11:18:51 Wair for tx: data:"\022&\n$/cosmos.bank.v1beta1.MsgSendResponse\032\010\000\000\000\000\000\000\372\235" log:"[{\"msg_index\":0,\"events\":[{\"type\":\"message\",\"attributes\":[{\"key\":\"action\",\"value\":\"/cosmos.bank.v1beta1.MsgSend\"},{\"key\":\"sender\",\"value\":\"0x525482AB3922230e4D73079890dC905dCc3D37cd\"},{\"key\":\"module\",\"value\":\"bank\"}]},{\"type\":\"coin_spent\",\"attributes\":[{\"key\":\"spender\",\"value\":\"0x525482AB3922230e4D73079890dC905dCc3D37cd\"},{\"key\":\"amount\",\"value\":\"10000000000BNB\"}]},{\"type\":\"coin_received\",\"attributes\":[{\"key\":\"receiver\",\"value\":\"0x525482AB3922230e4D73079890dC905dCc3D37cd\"},{\"key\":\"amount\",\"value\":\"10000000000BNB\"}]},{\"type\":\"transfer\",\"attributes\":[{\"key\":\"recipient\",\"value\":\"0x525482AB3922230e4D73079890dC905dCc3D37cd\"},{\"key\":\"sender\",\"value\":\"0x525482AB3922230e4D73079890dC905dCc3D37cd\"},{\"key\":\"amount\",\"value\":\"10000000000BNB\"}]},{\"type\":\"message\",\"attributes\":[{\"key\":\"sender\",\"value\":\"0x525482AB3922230e4D73079890dC905dCc3D37cd\"}]}]}]" gas_wanted:1200 gas_used:1200 events:<type:"coin_spent" attributes:<key:"spender" value:"0x525482AB3922230e4D73079890dC905dCc3D37cd" index:true > attributes:<key:"amount" value:"6000000000000BNB" index:true > > events:<type:"coin_received" attributes:<key:"receiver" value:"0xf1829676DB577682E944fc3493d451B67Ff3E29F" index:true > attributes:<key:"amount" value:"6000000000000BNB" index:true > > events:<type:"transfer" attributes:<key:"recipient" value:"0xf1829676DB577682E944fc3493d451B67Ff3E29F" index:true > attributes:<key:"sender" value:"0x525482AB3922230e4D73079890dC905dCc3D37cd" index:true > attributes:<key:"amount" value:"6000000000000BNB" index:true > > events:<type:"message" attributes:<key:"sender" value:"0x525482AB3922230e4D73079890dC905dCc3D37cd" index:true > > events:<type:"tx" attributes:<key:"fee" value:"6000000000000BNB" index:true > attributes:<key:"fee_payer" value:"0x525482AB3922230e4D73079890dC905dCc3D37cd" index:true > > events:<type:"tx" attributes:<key:"acc_seq" value:"0x525482AB3922230e4D73079890dC905dCc3D37cd/0" index:true > > events:<type:"tx" attributes:<key:"signature" value:"plUsfX6lsI0PLjPfFRY7RvYafQ9GK4gAh3pZHddcMdsR9wJRgKUVJ/JDy4HrIEI+qYHP1bGUOxWExmsVdab0xwE=" index:true > > events:<type:"message" attributes:<key:"action" value:"/cosmos.bank.v1beta1.MsgSend" index:true > attributes:<key:"sender" value:"0x525482AB3922230e4D73079890dC905dCc3D37cd" index:true > attributes:<key:"module" value:"bank" index:true > > events:<type:"coin_spent" attributes:<key:"spender" value:"0x525482AB3922230e4D73079890dC905dCc3D37cd" index:true > attributes:<key:"amount" value:"10000000000BNB" index:true > > events:<type:"coin_received" attributes:<key:"receiver" value:"0x525482AB3922230e4D73079890dC905dCc3D37cd" index:true > attributes:<key:"amount" value:"10000000000BNB" index:true > > events:<type:"transfer" attributes:<key:"recipient" value:"0x525482AB3922230e4D73079890dC905dCc3D37cd" index:true > attributes:<key:"sender" value:"0x525482AB3922230e4D73079890dC905dCc3D37cd" index:true > attributes:<key:"amount" value:"10000000000BNB" index:true > > events:<type:"message" attributes:<key:"sender" value:"0x525482AB3922230e4D73079890dC905dCc3D37cd" index:true > >
Make a storage deal¶
Storing data is one of the most important features of Greenfield. In this section, we’ll walk through the end-to-end process of storing your data on the Greenfield network. We’ll start by importing your data, then make a storage deal with a storage provider, and finally wait for the deal to complete.
1. Create a main file¶
Create a storage.go
file in your demo project and add the following boilerplate code:
func main() {
// initialize account
account, err := types.NewAccountFromPrivateKey("test", privateKey)
log.Println("address info:", account)
if err != nil {
log.Fatalf("New account from private key error, %v", err)
}
//initialize client
cli, err := client.New(chainId, rpcAddr, client.Option {DefaultAccount: account})
if err != nil {
log.Fatalf("unable to new greenfield client, %v", err)
}
ctx := context.Background()
// 1. choose storage provider
// 2. Create a bucket
// 3. Upload your data and set a quota
}
2. Choose your own SP¶
You can query the list of SP.
// get storage providers list
spLists, err := cli.ListStorageProviders(ctx, true)
if err != nil {
log.Fatalf("fail to list in service sps")
}
//choose the first sp to be the primary SP
primarySP := spLists[0].GetOperatorAddress()
3. Create your bucket¶
Bucket can be private or public. You can customize it with options. * VISIBILITY_TYPE_PUBLIC_READ * VISIBILITY_TYPE_PRIVATE
chargedQuota := uint64(10000000)
visibility := storageTypes.VISIBILITY_TYPE_PUBLIC_READ
opts := types.CreateBucketOptions{Visibility: visibility, ChargedQuota: chargedQuota}
bucketTx, err := cli.CreateBucket(ctx, bucketName, primarySP, opts)
if err != nil {
log.Fatalf("unable to send, %v", err)
}
log.Printf("Create bucket response: %s", bucketTx)
quota
work, read this doc.
4. Upload your object¶
Objects can also be private or public.
Uploading objects is composed of two parts: create and put.
* CreateObject
gets an approval of creating an object and sends createObject txn to Greenfield network.
* PutObject
supports the second stage of uploading the object to bucket.
// create and put object
var buffer bytes.Buffer
line := `0123456789`
for i := 0; i < objectSize/10; i++ {
buffer.WriteString(fmt.Sprintf("%s", line))
}
txnHash, err := cli.CreateObject(ctx, bucketName, objectName, bytes.NewReader(buffer.Bytes()), types.CreateObjectOptions{})
handleErr(err, "CreateObject")
err = cli.PutObject(ctx, bucketName, objectName, int64(buffer.Len()),
bytes.NewReader(buffer.Bytes()), types.PutObjectOptions{TxnHash: txnHash})
handleErr(err, "PutObject")
log.Printf("object: %s has been uploaded to SP\n", objectName)
waitObjectSeal(cli, bucketName, objectName)
func waitObjectSeal(cli client.Client, bucketName, objectName string) {
ctx := context.Background()
// wait for the object to be sealed
timeout := time.After(15 * time.Second)
ticker := time.NewTicker(2 * time.Second)
for {
select {
case <-timeout:
err := errors.New("object not sealed after 15 seconds")
handleErr(err, "HeadObject")
case <-ticker.C:
objectDetail, err := cli.HeadObject(ctx, bucketName, objectName)
handleErr(err, "HeadObject")
if objectDetail.ObjectInfo.GetObjectStatus().String() == "OBJECT_STATUS_SEALED" {
ticker.Stop()
fmt.Printf("put object %s successfully \n", objectName)
return
}
}
}
}
The primary SP syncs with secondary SPs to set up the data redundancy, and then it signs a Seal
transaction with the finalized metadata for storage. If the primary SP determines that it doesn’t want to store the file due to whatever reason, it can also “SealReject” the request.
5. Object management¶
5.1 Read object¶
You can call GetObject
function to download data.
// get object
reader, info, err := cli.GetObject(ctx, bucketName, objectName, types.GetObjectOptions{})
handleErr(err, "GetObject")
log.Printf("get object %s successfully, size %d \n", info.ObjectName, info.Size)
handleErr(err, "GetObject")
objectBytes, err := io.ReadAll(reader)
fmt.Printf("Read data: %s\n", string(objectBytes))
5.2 Update object visibility¶
- You can call
UpdateBucketVisibility
to change bucket visibility - You can call
UpdateObjectVisibility
to change object visibility
//update bucket visibility
updateBucketTx, err := cli.UpdateBucketVisibility(ctx, bucketName,
storageTypes.VISIBILITY_TYPE_PRIVATE, types.UpdateVisibilityOption{})
resp, err := cli.WaitForTx(ctx, updateBucketTx)
fmt.Printf("Update response: %s\n", resp)
handleErr(err, "UpdateBucketVisibility")
// Update object visibility
updateObjectTx, err := cli.UpdateObjectVisibility(ctx, bucketName,objectName,
storageTypes.VISIBILITY_TYPE_PRIVATE, types.UpdateObjectOption{})
resp, err := cli.WaitForTx(ctx, updateObjectTx)
fmt.Printf("Update response: %s\n", resp)
handleErr(err, "UpdateObjectVisibility")
```
#### 5.3 Delete object
The function DeleteObject support deleting objects.
```go
// delete object
delTx, err := cli.DeleteObject(ctx, bucketName, objectName, types.DeleteObjectOption{})
handleErr(err, "DeleteObject")
_, err = cli.WaitForTx(ctx, delTx)
if err != nil {
log.Fatalln("txn fail")
}
log.Printf("object: %s has been deleted\n", objectName)
Conclusion¶
Congratulations on making it all the way through this tutorial! In this tutorial, we learned the basics of interacting with the Greenfield network using SDK library.