massa-web3
Massa-web3
is a TypeScript library that allow you to interact with the Massa
blockchain through a
local or remote Massa node. In particular the massa-web3 library will allow you to call the JSON-RPC API,
but also to fetch and poll events from smart contracts on the Massa blockchain, deploy smart contracts and much more.
Usage
Massa-web3
could be used as a library for frameworks or as a stand-alone bundled js file which can be easily loaded into the browser.
Library (Node.js/React/Vue.js) usage
npm install @massalabs/massa-web3
Browser usage
Add the following script to your html file:
<script
type="text/javascript"
src="https://cdn.jsdelivr.net/npm/@massalabs/massa-web3@x.x.x/bundle.js"
></script>
whereby the x.x.x is one of the available released versions under Massa-web3's releases page:
In your code, once the script is fully loaded, just use window.massa
to access all massa-web3
exports.
<script>console.log("Massa Web3 ", window.massa);</script>
Documentation
Complete documentation of all available web3 entities can be found here:
Requirements
- NodeJS 14+
- npm / yarn (see package.json)
Web3 Client initialization
There are two types of client initialization. The first one is connecting to Massa's public rpc node using a so-called default client. Please note that specifying a base account is only optional at this point. The code below illustrates how to do that:
import {
ClientFactory,
Client,
DefaultProviderUrls,
IAccount,
} from "@massalabs/massa-web3";
// create a base account for signing transactions
const baseAccount = {
address: "A12PWTzCKkkE9P5Supt3Fkb4QVZ3cdfB281TGaup7Nv1DY12a6F1",
secretKey: "S12tw4YShWtjWfy7YBQ9Erbcg6DYgWnMgb5hGjn9hAKGtgrLNa7L",
publicKey: "P1hG8zRRJF2v3qkwyZ2fnHJeaVw9uT4huCkwcWJVvgypEz6D2aR",
} as IAccount;
// initialize a testnet client
const testnetClient: Client = await ClientFactory.createDefaultClient(
DefaultProviderUrls.TESTNET,
true,
baseAccount
);
The second way is to create a custom client connecting to a node whose ip and ports are to be specified by the user.
import {
ClientFactory,
Client,
IAccount,
IProvider,
ProviderType,
} from "@massalabs/massa-web3";
// create a base account for signing transactions
const baseAccount = {
address: "A12PWTzCKkkE9P5Supt3Fkb4QVZ3cdfB281TGaup7Nv1DY12a6F1",
secretKey: "S12tw4YShWtjWfy7YBQ9Erbcg6DYgWnMgb5hGjn9hAKGtgrLNa7L",
publicKey: "P1hG8zRRJF2v3qkwyZ2fnHJeaVw9uT4huCkwcWJVvgypEz6D2aR",
} as IAccount;
// initialize a custom client using an own provider
const providers: Array<IProvider> = [
{
url: "http://127.0.0.1:33035",
type: ProviderType.PUBLIC,
} as IProvider,
{
url: "http://127.0.0.1:33034",
type: ProviderType.PRIVATE,
} as IProvider,
];
const customClient: Client = await ClientFactory.createCustomClient(
providers,
true,
baseAccount
);
Please note that connecting to a locally running node could be easily done using the factory method:
const testnetClient: Client = await ClientFactory.createDefaultClient(
DefaultProviderUrls.LOCALNET,
true,
baseAccount
);
Once there is an initialized client instance, it is straightforward to call methods on it:
import { IStatus, IAddressInfo } from "@massalabs/massa-web3";
const addressesResp: Array<IAddressInfo> = await web3Client
.publicApi()
.getAddresses(["some_address"]);
Client exposed APIs
The client exposes several APIs which could be used on its own (also initialized as stand-alone) if one needs to:
web3Client.publicApi() -> sub-client for public api (interface: PublicApiClient)
web3Client.privateApi() -> sub-client for private api (interface: PrivateApiClient)
web3Client.wallet() -> sub-client for wallet-related operations (interface: WalletClient)
web3Client.smartContracts() -> sub-client for smart contracts interaction (interface: SmartContractsClient)
web3Client.ws() -> sub-client for websockets (interface: WsSubscriptionClient)
Client public API
Client public API operations are accessible under the public sub-client, which is accessible via the publicApi()
method on the client.
Example:
// get block info
const blocks: Array<IBlockInfo> = await web3Client
.publicApi()
.getBlocks(["q2XVw4HrRfwtX8FGXak2VwtTNkBvYtLVW67s8pTCVPdEEeG6J"]);
Available methods are:
-
getNodeStatus
const nodeStatus: INodeStatus = await web3Client .publicApi() .getNodeStatus();
-
getAddresses
const addressesResp: Array<IAddressInfo> = await web3Client .publicApi() .getAddresses(["A12PWTzCKkkE9P5Supt3Fkb4QVZ3cdfB281TGaup7Nv1DY12a6F1"]);
-
getBlocks
const blocks: Array<IBlockInfo> = await web3Client .publicApi() .getBlocks(["nKifcnGbd9zu8nu1hb94XEmMGwgoWbjj3DutzrobeHDdUtEuM"]);
-
getEndorsements
const endorsements: Array<IEndorsement> = await web3Client .publicApi() .getEndorsements(["q2XVw4HrRfwtX8FGXak2VwtTNkBvYtLVW67s8pTCVPdEEeG6J"]);
-
getOperations
const operations: Array<IOperationData> = await web3Client .publicApi() .getOperations(["z1cNsWAdgvoASq5RnN6MRbqqo634RRJbgwV9n3jNx3rQrQKTt"]);
-
getCliques
const cliques: Array<IClique> = await web3Client.publicApi().getCliques();
-
getStakers
const stakers: Array<IStakingAddresses> = await web3Client .publicApi() .getStakers();
-
getDatastoreEntries
const datastoreEntries: Array<IContractStorageData> = await web3Client .publicApi() .getDatastoreEntries([ { address: smartContractAddress, key: "some_key", } as IDatastoreEntry, ]);
getBlockcliqueBlockBySlot
const blockcliqueBlockBySlot: IBlockcliqueBlockBySlot = await web3Client
.publicApi()
.getBlockcliqueBlockBySlot([{ period: 12345, thread: 20 } as ISlot]);
getGraphInterval
const graphInterval: IGraphInterval = await web3Client
.publicApi()
.getGraphInterval([
{ start: Date.now() - 2000, end: Date.now() } as IGetGraphInterval,
]);
Client private API
Client private API operations are accessible under the private sub-client, which is accessible via the privateApi()
method on the client.
Example:
// stop the node
await web3Client.privateApi().nodeStop();
Available methods are:
-
stopNode
await web3Client.privateApi().nodeStop();
-
nodeBanById
await web3Client .privateApi() .nodeBanById("P1bZhWZQ2KW8DoaEqXyRXoy198wjhCsTFxSP53mLgdvx5C4WMDE");
-
nodeBanByIpAddress
await web3Client.privateApi().nodeBanByIpAddress("90.110.239.231");
-
nodeUnbanById
await web3Client .privateApi() .nodeUnbanById("P1bZhWZQ2KW8DoaEqXyRXoy198wjhCsTFxSP53mLgdvx5C4WMDE");
-
nodeUnbanByIpAddress
await web3Client.privateApi().nodeUnbanByIpAddress("90.110.239.231");
-
nodeGetStakingAddresses
const stakingAddresses = await web3Client .privateApi() .nodeGetStakingAddresses();
-
nodeRemoveStakingAddresses
await web3Client .privateApi() .nodeRemoveStakingAddresses([ "A12PWTzCKkkE9P5Supt3Fkb4QVZ3cdfB281TGaup7Nv1DY12a6F1", ]);
-
nodeAddStakingPrivateKeys
await web3Client .privateApi() .nodeAddStakingSecretKeys([ "S12tw4YShWtjWfy7YBQ9Erbcg6DYgWnMgb5hGjn9hAKGtgrLNa7L", ]);
-
nodeSignMessage
const message = "hello world"; const msgBuf = new TextEncoder().encode(message); const signedMessage = await web3Client.privateApi().nodeSignMessage(msgBuf);
-
nodeAddToPeersWhitelist
await web3Client.privateApi().nodeAddToPeersWhitelist("90.110.239.231");
-
nodeRemoveFromWhitelist
await web3Client.privateApi().nodeRemoveFromWhitelist("90.110.239.231");
Websockets API
Websocket subscriptions are accessible under the websockets client, which is accessible via the ws()
method on the web3 client. The massa-web3 library provides a convenient wrapper around browser and NodeJS compatible websockets client implementation.
Example:
import { WebsocketEvent } from "@massalabs/massa-web3";
// get the websockets client
const wsClient = web3Client.ws();
// bind various methods for handling common socket events
wsClient.on(WebsocketEvent.ON_CLOSED, () => {
console.log("ws closed");
});
wsClient.on(WebsocketEvent.ON_CLOSING, () => {
console.log("ws closing");
});
wsClient.on(WebsocketEvent.ON_CONNECTING, () => {
console.log("ws connecting");
});
wsClient.on(WebsocketEvent.ON_OPEN, () => {
console.log("ws open");
});
wsClient.on(WebsocketEvent.ON_PING, () => {
console.log("ws ping");
});
wsClient.on(WebsocketEvent.ON_ERROR, (errorMessage) => {
console.error("ws error", errorMessage);
});
// connect to ws
await wsClient.connect();
// subscribe to new blocks
wsClient.subscribeNewBlocks((newBlock) => {
console.log("New Block Received", newBlock);
});
Available common methods are:
-
connect
await wsClient.connect();
-
getBinaryType
wsClient.getBinaryType();
-
getBufferedAmount
wsClient.getBufferedAmount();
-
getExtensions
wsClient.getExtensions();
-
getProtocol
wsClient.getProtocol();
-
getReadyState
wsClient.getReadyState();
-
getUrl
wsClient.getUrl();
-
closeConnection
wsClient.closeConnection();
The following events are available to listen to:
```ts
wsSubClient.on(WebsocketEvent.ON_CLOSED, () => { ... });
wsSubClient.on(WebsocketEvent.ON_CLOSING, () => { ... });
wsSubClient.on(WebsocketEvent.ON_CONNECTING, () => { ... });
wsSubClient.on(WebsocketEvent.ON_OPEN, () => { ... });
wsSubClient.on(WebsocketEvent.ON_PING, () => { ... });
wsSubClient.on(WebsocketEvent.ON_ERROR, (errorMessage) => { ... });
```
The following subscription methods are available:
import { WebsocketEvent } from "@massalabs/massa-web3";
// get the websockets client
const wsClient = web3Client.ws();
// subscribe to new blocks
wsSubClient.subscribeNewBlocks((newBlock) => {
console.log(
"New Block Received \n",
newBlock as ISubscribeNewBlocksMessage
);
});
// unsubscribe to new blocks
wsSubClient.unsubscribeNewBlocks();
// subscribe to new blocks headers
wsSubClient.subscribeNewBlockHeaders((newBlockHeader) => {
console.log(
"New Block Header Received \n",
newBlockHeader as IBlockHeaderInfo
);
});
// unsubscribe to new blocks headers
wsSubClient.unsubscribeNewBlockHeaders();
// subscribe to new blocks
wsSubClient.subscribeFilledBlocks((newFilledBlock) => {
console.log(
"New Filled Block Received \n",
newFilledBlock as ISubscribeNewBlocksMessage
);
});
// unsubscribe to filled blocks
wsSubClient.unsubscribeFilledBlocks();
The underlying NodeJS websockets client API can be found here: NodeJS Websockets API and for the web browser: Browser Websockets API. The Massa Websockets Client is compatible with all currently available browsers.
Wallet operations
Wallet operations are accessible under the wallet sub-client which is accessible via the wallet() method on the client.
Example:
// generate new wallet
const newWalletAccount = await WalletClient.walletGenerateNewAccount();
Available class methods are:
-
addPrivateKeysToWallet
const addedAccounts: Array<IAccount> = await web3Client .wallet() .addSecretKeysToWallet([ "2SPTTLK6Vgk5zmZEkokqC3wgpKgKpyV5Pu3uncEGawoGyd4yzC", ]);
-
removeAddressesFromWallet
web3Client .wallet() .removeAddressesFromWallet([ "A12rr1neHvp7uzGepfPRPguZX5JWC3EFW6H7ZQRazzNjBRMNvQB", ]);
-
getWalletAccounts
const walletAccounts: Array<IAccount> = web3Client .wallet() .getWalletAccounts();
-
getWalletAccountByAddress
const walletAccount: IAccount | undefined = web3Client .wallet() .getWalletAccountByAddress( "A12rr1neHvp7uzGepfPRPguZX5JWC3EFW6H7ZQRazzNjBRMNvQB" );
-
addAccountsToWallet
await web3Client.wallet().addAccountsToWallet([ { address: "A12PWTzCKkkE9P5Supt3Fkb4QVZ3cdfB281TGaup7Nv1DY12a6F1", secretKey: "S12tw4YShWtjWfy7YBQ9Erbcg6DYgWnMgb5hGjn9hAKGtgrLNa7L", publicKey: "P1hG8zRRJF2v3qkwyZ2fnHJeaVw9uT4huCkwcWJVvgypEz6D2aR", }, ]);
-
walletInfo
const walletInfo: Array<IFullAddressInfo> = await web3Client .wallet() .walletInfo();
-
sendTransaction
const sendTxIds: Array<string> = await web3Client.wallet().sendTransaction( { fee: 0, // int amount: "1", //MAS recipientAddress: "A12PWTzCKkkE9P5Supt3Fkb4QVZ3cdfB281TGaup7Nv1DY12a6F1", } as ITransactionData, baseAccount );
-
buyRolls
const buyRollsIds: Array<string> = await web3Client.wallet().buyRolls( { fee: 0, // int amount: 1, //ROLLS } as IRollsData, baseAccount );
-
sellRolls
const sellRollsIds: Array<string> = await web3Client.wallet().sellRolls( { fee: 0, // int amount: 1, //ROLLS } as IRollsData, baseAccount );
-
getAccountBalance
const balance: IBalance = await web3Client .wallet() .getAccountBalance( "A12PWTzCKkkE9P5Supt3Fkb4QVZ3cdfB281TGaup7Nv1DY12a6F1" );
In addition to the class methods, there are also static methods for direct use:
-
getAccountFromPrivateKey
const account: IAccount = await WalletClient.getAccountFromSecretKey( "S12tw4YShWtjWfy7YBQ9Erbcg6DYgWnMgb5hGjn9hAKGtgrLNa7L" );
-
walletGenerateNewAccount
const newWalletAccount: IAccount = await WalletClient.walletGenerateNewAccount();
-
walletSignMessage
const sig: ISignature = await WalletClient.walletSignMessage( "hello", baseAccount );
Smart contract deployment
Once the smart contract WASM is available, it becomes quite straightforward to deploy a smart contract operation (a state changing operation):
// deploy smart contract
const opId: string = await web3Client.smartContracts().deploySmartContract(
{
fee: 0,
maxGas: 2000000,
contractDataBinary: compiledScFromSource.binary,
datastore: new Map<Uint8Array, Uint8Array>(),
} as IContractData,
baseAccount
);
The compiledScFromSource is the compiled smart contract code in binary form. The returned value is the resulting operation id.
Smart contract event fetching and polling
Emitted smart contract events could directly be fetched via:
const eventsFilter = {
start: null,
end: null,
original_caller_address:
"A12rr1neHvp7uzGepfPRPguZX5JWC3EFW6H7ZQRazzNjBRMNvQB",
original_operation_id: null,
emitter_address: null,
} as IEventFilter;
const filteredEvents: Array<IEvent> = await web3Client
.smartContracts()
.getFilteredScOutputEvents(eventFilterData);
Events could also be polled. The js sdk has two methods for doing this as shown below. In both, a filter, a web3 client and a poll interval which we can set in order to poll the events needs to be provided:
const onEventData = (events: Array<IEvent>) => {
console.log("Event Data Received:", events);
};
const onEventDataError = (error: Error) => {
console.log("Event Data Error:", error);
};
// poll smart contract events
const eventsFilter = {
start: null,
end: null,
original_caller_address:
"A12rr1neHvp7uzGepfPRPguZX5JWC3EFW6H7ZQRazzNjBRMNvQB",
original_operation_id: null,
emitter_address: null,
is_final: true,
} as IEventFilter;
const eventPoller = EventPoller.startEventsPolling(
eventsFilter,
1000,
web3Client
);
eventPoller.on(ON_MASSA_EVENT_DATA, onEventData);
eventPoller.on(ON_MASSA_EVENT_ERROR, onEventDataError);
//...do some work...
// cleanup and finish
eventPoller.stopPolling();
Alternatively, one could make direct use of callback functions as function arguments which would fire on event data received or generated errors:
const onEventData = (events: Array<IEvent>) => {
console.log("Event Data Received:", events);
};
const onEventDataError = (error: Error) => {
console.log("Event Data Error:", error);
};
const eventPoller: EventPoller = EventPoller.startEventsPolling(
eventsFilter,
1000,
web3Client,
onEventData,
onEventDataError
);
//...do some work...
// cleanup and finish
eventPoller.stopPolling();
The latter could easily be employed in smart contracts where we need to e.g. get the contract address. For example, this contract would emit the address at creation:
import { call, print, create_sc, generate_event } from "massa-sc-std";
export function main(_args: string): i32 {
... deploy the smart contract ...
generateEvent(`Address:${sc_address}`); //emit an event with the address
...
}
Smart contract blockchain status
Smart contracts undergo various transaction statuses before they reach block finality on chain. The public enum describing these statuses is:
EOperationStatus {
INCLUDED_PENDING,
AWAITING_INCLUSION,
FINAL,
INCONSISTENT,
NOT_FOUND
}
The current smart contract status could be easily obtained via:
const status: EOperationStatus = await web3Client
.smartContracts()
.getOperationStatus(deploymentOperationId);
There are however cases when one would require to await a given status and that could be done via. It is important to note here that the algorithm will giv up after a certain amount of time or a limited error count. These values have proven to be sufficient for most standard cases.
const status: EOperationStatus = await web3Client
.smartContracts()
.awaitRequiredOperationStatus(
deploymentOperationId,
EOperationStatus.INCLUDED_PENDING
);
Smart contract balance
Smart contract balances could be easily obtained via using the getContractBalance
method:
const balance: IBalance | null = await web3Client
.smartContracts()
.getContractBalance(contractAddress);
Smart contract read and write calls
Smart contract data could be read via readSmartContract
method:
const data: IContractReadOperationResponse = await web3Client
.smartContracts()
.readSmartContract({
fee: 0,
maxGas: 200000,
targetAddress: scAddress,
targetFunction: "getGameState",
parameter: new Args().serialize(), // this is based on input arguments
} as IReadData);
The returned data is contained in an object of type IContractReadOperationResponse under the key returnedValue
which is of type Uint8Array. Depending on the smart contract function implementation, the user is to convert the latter into the expected data type.
Smart contract state-changing operations could be executed via callSmartContract
method:
const data: string = await web3Client.smartContracts().callSmartContract(
{
fee: 0,
maxGas: 200000,
coins: new MassaCoin(0),
targetAddress: scAddress,
functionName: "play",
parameter: new Args().serialize(), // this is based on input arguments
} as ICallData,
baseAccount
);
The returned value is the operation id.
Smart contracts could also be constructed in order to read data from another contract. In that case one could use the code below to read the data via a proxy contract:
// read smart contract data
const data: IExecuteReadOnlyResponse = await web3Client
.smartContracts()
.executeReadOnlySmartContract(
{
fee: 0,
maxGas: 2000000,
coins: new MassaCoin(0),
contractDataBinary: compiledScFromSource.binary,
} as IContractData,
baseAccount
);
The returned data is contained in an object of type IExecuteReadOnlyResponse under the key returnedValue
which is of type Uint8Array. Depending on the smart contract function implementation, the user is to convert the latter into the expected data type.
Contributing and testing
- Run
npm run install
to install all deps - Run
npm run build
to build distribution content - Run
npm run test
to run integration and unit tests