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¶
import { Client } from '@bnb-chain/greenfield-js-sdk';
const client = Client.create('https://gnfd-testnet-fullnode-tendermint-ap.bnbchain.org, '5600');
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>
;(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.
;(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:
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:
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;
};
;(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
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,
},
);
;(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.