Skip to content

Building File Management Tool with Greenfield SDK (JS)

Several Chain API libraries are available. These libraries manage the low-level logic of connecting to the Greenfield node, making requests, and handing the responses.

In this tutorial, we’ll use the JS-SDK library to interact with testnet.

Prerequisites

Before getting started, you should be familiar with:

  • Greenfield basics
  • Follow the instructions provided in Token Transfer. Please be aware that if your account does not have any BNB, the transaction will not be executed.

Setup

Create Project

Follow Quick Start to create the project.

Create a new index.js:

```bash title="Nodejs create project"
> mkdir gnfd-app
> cd gnfd-app
> touch index.js
```

Install SDK:

```bash title="npm install deps"
> npm init -y
> npm add @bnb-chain/greenfield-js-sdk
```

Create Greenfield Client

Create testnet Client
import { Client } from '@bnb-chain/greenfield-js-sdk';

const client = Client.create('https://gnfd-testnet-fullnode-tendermint-ap.bnbchain.org, '5600');
Create testnet client
const {Client}  = require('@bnb-chain/greenfield-js-sdk');

// testnet
const client = Client.create('https://gnfd-testnet-fullnode-tendermint-ap.bnbchain.org', '5600');

Test a simple function

<button
className="button is-primary"
onClick={async () => {
    const latestBlockHeight = await client.basic.getLatestBlockHeight();

    alert(JSON.stringify(latestBlockHeight));
}}
>
getLatestBlockHeight
</button>
index.js
;(async () => {
const latestBlockHeight = await client.basic.getLatestBlockHeight()

console.log('latestBlockHeight', latestBlockHeight)
})()

Run index.js to get the latest block height:

> node index.js

This will output like:

latestBlockHeight 3494585

Get Address balance

In the previous step, we verified that the client was OK.

Now we try more features for an account.

<button
className="button is-primary"
onClick={async () => {
    if (!address) return;

    const balance = await client.account.getAccountBalance({
    address: address,
    denom: 'BNB',
    });

    alert(JSON.stringify(balance));
}}
>
getAccountBalance
</button>

You can query an account’s balance by calling account.getAccountBalance function.

get account's balance
;(async () => {
    const balance = await client.account.getAccountBalance({
    address: '0x1C893441AB6c1A75E01887087ea508bE8e07AAae',
    denom: 'BNB'
})

console.log('balance: ', balance)
})()

Run node index.js to get the account’s balance:

balance: { balance: { denom: 'BNB', amount: '4804586044359520195' } }

Apart from the basic data queries shown above, there are many more features. Please see the API Reference for all Greenfield API definitions.

Manage Wallet

The wallet in Nodejs is generated by a private key, but the wallet plugin(MetaMask, CollectWallet, etc) can’t get the user’s private key in the browser, so another way is needed.

<button
className="button is-primary"
onClick={async () => {
    if (!address) return;

    const transferTx = await client.account.transfer({
    fromAddress: address,
    toAddress: '0x0000000000000000000000000000000000000000',
    amount: [
        {
        denom: 'BNB',
        amount: '1000000000',
        },
    ],
    });

    const simulateInfo = await transferTx.simulate({
    denom: 'BNB',
    });

    const res = await transferTx.broadcast({
    denom: 'BNB',
    gasLimit: Number(simulateInfo.gasLimit),
    gasPrice: simulateInfo.gasPrice,
    payer: address,
    granter: '',
    signTypedDataCallback: async (addr: string, message: string) => {
        const provider = await connector?.getProvider();
        return await provider?.request({
        method: 'eth_signTypedData_v4',
        params: [addr, message],
        });
    },
    });

    if (res.code === 0) {
    alert('transfer success!!');
    }
}}
>
transfer
</button>

In general, we need to put the private key in the .env file and ignore this file in the .gitignore file (for account security).

> touch .env

Add this information to .env:

# fill your account info
ACCOUNT_PRIVATEKEY=0x...
ACCOUNT_ADDRESS=0x...

Install dotenv dependencies(for loading variables from .env):

> npm install dotenv

Everything is ready and we can transfer the transaction now.

Create transfer.js file:

transfer.js
require('dotenv').config();
const {Client} = require('@bnb-chain/greenfield-js-sdk');
const client = Client.create('https://gnfd-testnet-fullnode-tendermint-ap.bnbchain.org', '5600');

;(async () => {

// construct tx
const transferTx = await client.account.transfer({
    fromAddress: process.env.ACCOUNT_ADDRESS,
    toAddress: '0x0000000000000000000000000000000000000000',
    amount: [
    {
        denom: 'BNB',
        amount: '1000000000',
    },
    ],
})

// simulate transfer tx
const simulateInfo = await transferTx.simulate({
    denom: 'BNB',
});

// broadcast transfer tx
const res = await transferTx.broadcast({
    denom: 'BNB',
    gasLimit: Number(simulateInfo.gasLimit),
    gasPrice: simulateInfo.gasPrice,
    payer: process.env.ACCOUNT_ADDRESS,
    granter: '',
    privateKey: process.env.ACCOUNT_PRIVATEKEY,
})

console.log('res', res)
})()

Running node transfer.js:

transfer tx response

{
code: 0,
height: 3495211,
txIndex: 0,
events: [
    { type: 'coin_spent', attributes: [Array] },
    { type: 'coin_received', attributes: [Array] },
    { type: 'transfer', attributes: [Array] },
    { type: 'message', attributes: [Array] },
    { type: 'tx', attributes: [Array] },
    { type: 'tx', attributes: [Array] },
    { type: 'tx', attributes: [Array] },
    { type: 'message', attributes: [Array] },
    { type: 'coin_spent', attributes: [Array] },
    { type: 'coin_received', attributes: [Array] },
    { type: 'transfer', attributes: [Array] },
    { type: 'message', attributes: [Array] }
],
rawLog: '[{"msg_index":0,"events":[{"type":"message","attributes":[{"key":"action","value":"/cosmos.bank.v1beta1.MsgSend"},{"key":"sender","value":"0x1C893441AB6c1A75E01887087ea508bE8e07AAae"},{"key":"module","value":"bank"}]},{"type":"coin_spent","attributes":[{"key":"spender","value":"0x1C893441AB6c1A75E01887087ea508bE8e07AAae"},{"key":"amount","value":"1000000000BNB"}]},{"type":"coin_received","attributes":[{"key":"receiver","value":"0x0000000000000000000000000000000000000000"},{"key":"amount","value":"1000000000BNB"}]},{"type":"transfer","attributes":[{"key":"recipient","value":"0x0000000000000000000000000000000000000000"},{"key":"sender","value":"0x1C893441AB6c1A75E01887087ea508bE8e07AAae"},{"key":"amount","value":"1000000000BNB"}]},{"type":"message","attributes":[{"key":"sender","value":"0x1C893441AB6c1A75E01887087ea508bE8e07AAae"}]}]}]',
transactionHash: '1B731E99A55868F773E9A7C951D9325BE7995616B990924D47491320599789DE',
msgResponses: [
    {
    typeUrl: '/cosmos.bank.v1beta1.MsgSendResponse',
    value: Uint8Array(0) []
    }
],
gasUsed: 1200n,
gasWanted: 1200n
}

More TxClient References.

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.

0. Create the main file

The browser doesn’t need the main file.

> touch storage.js

1. Choose your own SP

You can query the list of SP:

utils/offchainAuth.ts
export const getSps = async () => {
const sps = await client.sp.getStorageProviders();
const finalSps = (sps ?? []).filter((v: any) => v.endpoint.includes('nodereal'));

return finalSps;
};

export const getAllSps = async () => {
const sps = await getSps();

return sps.map((sp) => {
    return {
    address: sp.operatorAddress,
    endpoint: sp.endpoint,
    name: sp.description?.moniker,
    };
});
};

export const selectSp = async () => {
const finalSps = await getSps();

const selectIndex = Math.floor(Math.random() * finalSps.length);

const secondarySpAddresses = [
    ...finalSps.slice(0, selectIndex),
    ...finalSps.slice(selectIndex + 1),
].map((item) => item.operatorAddress);
const selectSpInfo = {
    id: finalSps[selectIndex].id,
    endpoint: finalSps[selectIndex].endpoint,
    primarySpAddress: finalSps[selectIndex]?.operatorAddress,
    sealAddress: finalSps[selectIndex].sealAddress,
    secondarySpAddresses,
};

return selectSpInfo;
};

const getOffchainAuthKeys = async (address: string, provider: any) => {
const storageResStr = localStorage.getItem(address);

if (storageResStr) {
    const storageRes = JSON.parse(storageResStr) as IReturnOffChainAuthKeyPairAndUpload;
    if (storageRes.expirationTime < Date.now()) {
    alert('Your auth key has expired, please generate a new one');
    localStorage.removeItem(address);
    return;
    }

    return storageRes;
}

const allSps = await getAllSps();
const offchainAuthRes = await client.offchainauth.genOffChainAuthKeyPairAndUpload(
    {
    sps: allSps,
    chainId: GREEN_CHAIN_ID,
    expirationMs: 5 * 24 * 60 * 60 * 1000,
    domain: window.location.origin,
    address,
    },
    provider,
);

const { code, body: offChainData } = offchainAuthRes;
if (code !== 0 || !offChainData) {
    throw offchainAuthRes;
}

localStorage.setItem(address, JSON.stringify(offChainData));
return offChainData;
};
storage.js
;(async () => {
// get storage providers list
const sps = await client.sp.getStorageProviders()

// choose the first up to be the primary SP
const primarySP = sps[0].operatorAddress;
})()

2. Create your bucket

Bucket can be private or public. You can customize it with options.

VisibilityType:

  • VISIBILITY_TYPE_PUBLIC_READ
  • VISIBILITY_TYPE_PRIVATE
create bucket
import { Long, VisibilityType, RedundancyType, bytesFromBase64 } from '@bnb-chain/greenfield-js-sdk';
const createBucketTx = await client.bucket.createBucket(
  {
    bucketName: info.bucketName,
    creator: address,
    visibility: VisibilityType.VISIBILITY_TYPE_PUBLIC_READ,
    chargedReadQuota: Long.fromString('0'),
    primarySpAddress: spInfo.primarySpAddress,
    paymentAddress: address,
  },
);

const simulateInfo = await createBucketTx.simulate({
  denom: 'BNB',
});

console.log('simulateInfo', simulateInfo);

const res = await createBucketTx.broadcast({
  denom: 'BNB',
  gasLimit: Number(simulateInfo?.gasLimit),
  gasPrice: simulateInfo?.gasPrice || '5000000000',
  payer: address,
  granter: '',
});

3. Create Object and Upload Object

Objects can also be private or public.

Getting the file’s checksum needs reed-solomon:

import { ReedSolomon } from '@bnb-chain/reed-solomon';

const rs = new ReedSolomon();

// file is File type
const fileBytes = await file.arrayBuffer();
const expectCheckSums = rs.encode(new Uint8Array(fileBytes));
const fs = require('node:fs');
const { NodeAdapterReedSolomon } = require('@bnb-chain/reed-solomon/node.adapter');

const filePath = './CHANGELOG.md';
const fileBuffer = fs.readFileSync(filePath);
const rs = new NodeAdapterReedSolomon();
const expectCheckSums = await rs.encodeInWorker(__filename, Uint8Array.from(fileBuffer));

Getting approval of creating an object and sending createObject txn to the Greenfield network:

const createObjectTx = await client.object.createObject(
  {
    bucketName: info.bucketName,
    objectName: info.objectName,
    creator: address,
    visibility: VisibilityType.VISIBILITY_TYPE_PRIVATE,
    contentType: fileType,
    redundancyType: RedundancyType.REDUNDANCY_EC_TYPE,
    payloadSize: Long.fromInt(fileBuffer.length),
    expectChecksums: expectCheckSums.map((x) => bytesFromBase64(x)),
  },
);

const simulateInfo = await createObjectTx.simulate({
  denom: 'BNB',
});

const res = await createObjectTx.broadcast({
  denom: 'BNB',
  gasLimit: Number(simulateInfo?.gasLimit),
  gasPrice: simulateInfo?.gasPrice || '5000000000',
  payer: address,
  granter: '',
});

Upload Object:

await client.object.uploadObject(
{
    bucketName: info.bucketName,
    objectName: info.objectName,
    body: info.file,
    txnHash: txnHash,
},
{
    type: 'EDDSA',
    domain: window.location.origin,
    seed: offChainData.seedString,
    address,
},
);
await client.object.uploadObject(
{
    bucketName: bucketName,
    objectName: objectName,
    body: createFile(filePath),
    txnHash: createObjectTxRes.transactionHash,
},
{
    type: 'ECDSA',
    privateKey: ACCOUNT_PRIVATEKEY,
}
);

// convert buffer to file
function createFile(path) {
const stats = fs.statSync(path);
const fileSize = stats.size;

return {
    name: path,
    type: '',
    size: fileSize,
    content: fs.readFileSync(path),
}
}

4. Object management

4.1 Download object

await client.object.downloadFile(
{
    bucketName: info.bucketName,
    objectName: info.objectName,
},
{
    type: 'EDDSA',
    address,
    domain: window.location.origin,
    seed: offChainData.seedString,
},
);
manage.js
;(async () => {

// download object
const res = await client.object.getObject({
    bucketName: 'extfkdcxxd',
    objectName: 'yhulwcfxye'
}, {
    type: 'ECDSA',
    privateKey: ACCOUNT_PRIVATEKEY,
})

// res.body is Blob
console.log('res', res)
const buffer = Buffer.from([res.body]);
fs.writeFileSync('your_output_file', buffer)
})()

4.2 Update object visibility

const tx = await client.object.updateObjectInfo({
bucketName: info.bucketName,
objectName: info.objectName,
operator: address,
visibility: 1,
});

const simulateTx = await tx.simulate({
denom: 'BNB',
});

const res = await tx.broadcast({
denom: 'BNB',
gasLimit: Number(simulateTx?.gasLimit),
gasPrice: simulateTx?.gasPrice || '5000000000',
payer: address,
granter: '',
});
const tx = await client.object.updateObjectInfo({
bucketName: 'extfkdcxxd',
objectName: 'yhulwcfxye',
operator: ACCOUNT_ADDRESS,
visibility: 1,
})

const simulateTx = await tx.simulate({
denom: 'BNB',
})

const createObjectTxRes = await tx.broadcast({
denom: 'BNB',
gasLimit: Number(simulateTx?.gasLimit),
gasPrice: simulateTx?.gasPrice || '5000000000',
payer: ACCOUNT_ADDRESS,
granter: '',
privateKey: ACCOUNT_PRIVATEKEY,
});

4.3 Delete Object

const tx = await client.object.deleteObject({
bucketName: info.bucketName,
objectName: info.objectName,
operator: address,
});

const simulateTx = await tx.simulate({
denom: 'BNB',
});

const res = await tx.broadcast({
denom: 'BNB',
gasLimit: Number(simulateTx?.gasLimit),
gasPrice: simulateTx?.gasPrice || '5000000000',
payer: address,
granter: '',
});
;(async () => {
const tx = await client.object.deleteObject({
    bucketName: 'extfkdcxxd',
    objectName: 'yhulwcfxye',
    operator: ACCOUNT_ADDRESS,
});

const simulateTx = await tx.simulate({
    denom: 'BNB',
})

const createObjectTxRes = await tx.broadcast({
    denom: 'BNB',
    gasLimit: Number(simulateTx?.gasLimit),
    gasPrice: simulateTx?.gasPrice || '5000000000',
    payer: ACCOUNT_ADDRESS,
    granter: '',
    privateKey: ACCOUNT_PRIVATEKEY,
});

if (createObjectTxRes.code === 0) {
    console.log('delete object success')
}
})()

Conclusion

Congratulations on making it through this tutorial! In this tutorial, we learned the basics of interacting with the Greenfield network using the SDK library.

Tutorials Source Code