Skip to main content

Building a C NEAR library

· 14 min read
Damian
Docs Maintainer

You can now use the C language to interact with the NEAR blockchain!

The cNEAR library​

Recently I have been working in a client library to interact with the NEAR blockchain using the C language. Since this library is written in C, and there are C compilers for many processor architectures, it allows different devices beyond the usual web-browser on PC (like a WiFi router or a gaming console) to interact with the Near blockchain. The only requirement (beyond a C compiler) is the libcurl library, that leverages the networking communication with the RPC servers.

At the current development stage, the cNEAR library lets you interact with smart contracts deployed on the Near blockchain (either testnet or mainnet), and call or view contract methods. When calling methods that change the contract state, remember that you need to initialize the library with a blockchain account (using a private/public key pair) so the library can sign the transaction, pay the gas fees, and execute the contract method.

As mentioned, this C library is still an early development proof of concept, so more features could be added in the future to support other blockchain actions such as token transfers or batch transactions.

info

If you find bugs or want to request additional features, feel free to open an issue on the GitHub repository.

How to use it​

Now let's review the basic usage, with some code examples:

  • Initialize the library and set the RPC server:
// init the library
if (!near_rpc_init("https://rpc.testnet.near.org", true))
{
printf("Error initializing RPC");
}
  • Set the Near account used to sign transactions:
#define TEST_PUBKEY     "ed25519:FY835wAj7g8fRMncf4tqkyT3YdoW71t1ERnt3L78R28i"
#define TEST_PRVKEY "ed25519:36gU64pAbTLH6uuxUeGvwF8n3fUfnDRSC87Z7Q5Ez2WdhcCy2KB6KtGX1WDcym6VezUhojWN4waBiwFAvxtXXNJN"

// set the account keys
if (!near_account_init("my-account.testnet", TEST_PRVKEY, TEST_PUBKEY))
{
printf("Error initializing account");
}
  • Call a smart contract on the NEAR blockchain and get the current state:
cnearResponse result;
// Call a contract function 'get_greeting' on contract 'demo-devhub-vid102.testnet'
// with empty JSON arguments
// Note: this is a view function (read-only), so it doesn't require gas
result = near_rpc_call_function("demo-devhub-vid102.testnet", "get_greeting", "{}");

if(result.rpc_code == 200)
{
size_t len;
char* contract_msg = (char*) near_decode_result(&result, &len);

if (contract_msg)
{
printf("---\n%s\n---\n", contract_msg);
free(res);
}
}
free(result.json);
  • Call a smart contract on the NEAR blockchain and set a new state:
cnearResponse result;
// Call a contract function 'set_greeting' on contract 'demo-devhub-vid102.testnet'
// with JSON arguments '{"greeting":"Hello cNEAR!"}'
// Note: this is a change method that modifies the contract's state,
// so it's a signed transaction (require gas, and a signer account)
result = near_contract_call("demo-devhub-vid102.testnet",
"set_greeting",
"{\"greeting\":\"Hello cNEAR!\"}",
NEAR_DEFAULT_100_TGAS, 0, NEAR_TX_STATUS_EXEC_OPTIMISTIC);

The library provides other methods too, that you can check here.

Example PS3 App​

To showcase how to use the cNEAR library, and to prove that it can be used to build applications in different platforms, I created a sample PlayStation 3 app that uses the library, connects to a simple smart contract running on the NEAR testnet network, and gets or sets the current greeting message on the contract's state.

Some notes about the portability that the C library enables in this example app:

Building your own library​

For those developers that don't use C, but perhaps would like to use a different programming language (like Java, C#, Go) and integrate the NEAR blockchain in their projects, I'll present a short road map on how to build your own client library from scratch.

Basics​

On a high-level, a client library needs to communicate with a NEAR node using the JSON-RPC API, by encoding requests and decoding responses in the expected JSON format for each method. In the same way, tools like near-cli and near-api-js are just abstractions making RPC calls.

So, if you want to build your own client library, you'll need to:

  • handle HTTP connections to the RPC server.
    • For the cNEAR library, I used libcurl to handle the HTTP network communications. Your language might provide specific libraries for HTTP requests.
  • handle JSON messages and responses to communicate with the RPC server.
    • For the cNEAR library, I used cJSON, but your language might have support for JSON serialization through native libraries or packages.
  • handle Base64 and Base58 encodings, as some NEAR fields use these encodings.
  • handle Borsh serialization, as NEAR transactions are binary encoded in this specific serialization standard.
    • For the cNEAR library, I did my own Borsh encoding routines. Borsh deserialization isn't needed if you're just building a client.
  • handle ed25519 signatures, so your client can sign transactions using a NEAR account's private key.

Process flow​

Now, let's see how the process flow can be implemented so the client code can achieve a successful interaction with the NEAR network.

For this example, we aim to call a smart contract method set_greeting, set a new greeting value "hello world", and sign the transaction with a testnet account. If we dig into the NEAR protocol documentation, we'll find that to call a contract that changes the blockchain state, we need to create a Signed Transaction with a FunctionCall Action that specifies the contract's method and arguments.

tip

You can find more information about Transactions and Actions on the Nomicon protocol documentation site.

Let's use the near-cli with the awesome --teach-me parameter, so the CLI will show us the whole process between the CLI and the RPC server.

near --teach-me contract call-function as-transaction demo-devhub-vid102.testnet set_greeting json-args ' {"greeting" : "hello world" }' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' sign-as solops2.testnet network-config testnet sign-with-legacy-keychain display

Once you execute the near command, you'll get a low-level detail of the whole process to generate a base64-encoded signed transaction:

Unsigned transaction:

signer_id: solops2.testnet
receiver_id: demo-devhub-vid102.testnet
actions:
-- function call:
method name: set_greeting
args: {
"greeting": "hello world"
}
gas: 100.0 Tgas
deposit: 0 NEAR

INFO Signing the transaction with a key saved in legacy keychain ...:Getting access key information:: public key ed25519:6pEzqbRe2XnSPjQ7K7UpApfszDDcbY8RQJdVkaFXNevD on account <solops2.testnet>...
INFO I am making HTTP call to NEAR JSON RPC to get an access key details for public key ed25519:6pEzqbRe2XnSPjQ7K7UpApfszDDcbY8RQJdVkaFXNevD on account <solops2.testnet>, learn more https://docs.near.org/api/rpc/access-keys#view-access-key
INFO HTTP POST https://archival-rpc.testnet.near.org/
INFO JSON Request Body:
| {
| "id": "WeqIvFM8f",
| "jsonrpc": "2.0",
| "method": "query",
| "params": {
| "account_id": "solops2.testnet",
| "finality": "optimistic",
| "public_key": "ed25519:6pEzqbRe2XnSPjQ7K7UpApfszDDcbY8RQJdVkaFXNevD",
| "request_type": "view_access_key"
| }
| }
INFO JSON RPC Response:
| {
| "block_hash": "DTAXqtkUdpUp1oaPThYVMLYu1QnT2KzQFhisuQ3iCsSp",
| "block_height": 179596261,
| "nonce": 66097883000050,
| "permission": "FullAccess"
| }

You can see that the CLI calls the RPC server, and using the public key from the testnet account, it requests the RPC view_access_key method. The response provides two values that are required to build the transaction:

  • block_hash = DTAXqtkUdpUp1oaPThYVMLYu1QnT2KzQFhisuQ3iCsSp
  • nonce = 66097883000050

Using those values, the CLI generates a transaction with a FunctionCall action that calls set_greeting, serializes it using Borsh, and then signs it with the private key of the testnet account.

Your transaction was signed successfully.
Public key: ed25519:6pEzqbRe2XnSPjQ7K7UpApfszDDcbY8RQJdVkaFXNevD
Signature: ed25519:3uUiouFRtEWLURdq7itnYL8KVNLdKYiua3zv2xxoJpqTNghmgdzMMqL6FxsovyWSbQJqE5oBsMKGcAKBrE2499Tz

Signed transaction (serialized as base64):
DwAAAHNvbG9wczIudGVzdG5ldABWZI8xXoQFnilyGUEmEPxsqxg1hcdnIY2lLY+snNEf6vMs1pwdPAAAGgAAAGRlbW8tZGV2aHViLXZpZDEwMi50ZXN0bmV0uQAfK/w+8BKQJhwJzbP0lq4q217VghNNcGLtlzKkZykBAAAAAgwAAABzZXRfZ3JlZXRpbmcaAAAAeyJncmVldGluZyI6ImhlbGxvIHdvcmxkIn0AQHoQ81oAAAAAAAAAAAAAAAAAAAAAAAAAkUn66wC30xf2CFUjFE8oOlArYiinVPBHau+zTbP8iFWq/xJ9QM9TWpTG9VmWi31H+hQwCEE0lQlDxPNx5gdbDQ==
How a signature is generated

If you base64-decode the signed transaction and take a look at the binary data, you'll see:

00000000  0f 00 00 00 73 6f 6c 6f  70 73 32 2e 74 65 73 74  |....solops2.test|
00000010 6e 65 74 00 56 64 8f 31 5e 84 05 9e 29 72 19 41 |net.Vd.1^...)r.A|
00000020 26 10 fc 6c ab 18 35 85 c7 67 21 8d a5 2d 8f ac |&..l..5..g!..-..|
00000030 9c d1 1f ea f3 2c d6 9c 1d 3c 00 00 1a 00 00 00 |.....,...<......|
00000040 64 65 6d 6f 2d 64 65 76 68 75 62 2d 76 69 64 31 |demo-devhub-vid1|
00000050 30 32 2e 74 65 73 74 6e 65 74 b9 00 1f 2b fc 3e |02.testnet...+.>|
00000060 f0 12 90 26 1c 09 cd b3 f4 96 ae 2a db 5e d5 82 |...&.......*.^..|
00000070 13 4d 70 62 ed 97 32 a4 67 29 01 00 00 00 02 0c |.Mpb..2.g)......|
00000080 00 00 00 73 65 74 5f 67 72 65 65 74 69 6e 67 1a |...set_greeting.|
00000090 00 00 00 7b 22 67 72 65 65 74 69 6e 67 22 3a 22 |...{"greeting":"|
000000a0 68 65 6c 6c 6f 20 77 6f 72 6c 64 22 7d 00 40 7a |hello world"}.@z|
000000b0 10 f3 5a 00 00 00 00 00 00 00 00 00 00 00 00 00 |..Z.............|
000000c0 00 00 00 00 00 00 91 49 fa eb 00 b7 d3 17 f6 08 |.......I........|
000000d0 55 23 14 4f 28 3a 50 2b 62 28 a7 54 f0 47 6a ef |U#.O(:P+b(.T.Gj.|
000000e0 b3 4d b3 fc 88 55 aa ff 12 7d 40 cf 53 5a 94 c6 |.M...U...}@.SZ..|
000000f0 f5 59 96 8b 7d 47 fa 14 30 08 41 34 95 09 43 c4 |.Y..}G..0.A4..C.|
00000100 f3 71 e6 07 5b 0d |.q..[.|

The process to generate the signature is:

  • take the unsigned binary transaction (first 197 bytes, offset 0x00 to 0xC5)
  • calculate the SHA256 hash of the unsigned transaction:
    e1527deb5660c739c61080998b81bd0dfa05eb8a239b07f99a307561923c1168
  • generate the ED25519 signature using the SHA256 hash as input buffer:
    9149faeb00b7d317f6085523144f283a502b6228a754f0476aefb34db3fc8855aaff127d40cf535a94c6f559968b7d47fa1430084134950943c4f371e6075b0d

You can use the CLI to print the details of this base64-encoded signed transaction:

near transaction print-transaction signed 'DwAAAHNvbG9wczIudGVzdG5ldABWZI8xXoQFnilyGUEmEPxsqxg1hcdnIY2lLY+snNEf6vMs1pwdPAAAGgAAAGRlbW8tZGV2aHViLXZpZDEwMi50ZXN0bmV0uQAfK/w+8BKQJhwJzbP0lq4q217VghNNcGLtlzKkZykBAAAAAgwAAABzZXRfZ3JlZXRpbmcaAAAAeyJncmVldGluZyI6ImhlbGxvIHdvcmxkIn0AQHoQ81oAAAAAAAAAAAAAAAAAAAAAAAAAkUn66wC30xf2CFUjFE8oOlArYiinVPBHau+zTbP8iFWq/xJ9QM9TWpTG9VmWi31H+hQwCEE0lQlDxPNx5gdbDQ=='

You can see that the signed transaction has all the details about the smart contract, the method, arguments, the public key, block hash, and the incremented nonce (nonce++):

Signed transaction (full):

signature: ed25519:3uUiouFRtEWLURdq7itnYL8KVNLdKYiua3zv2xxoJpqTNghmgdzMMqL6FxsovyWSbQJqE5oBsMKGcAKBrE2499Tz

Unsigned transaction hash (Base58-encoded SHA-256 hash): GAZgvCCbnbRhZx5gcdmdHL83XFairbJrUfcJxu8NxtR5


public_key: ed25519:6pEzqbRe2XnSPjQ7K7UpApfszDDcbY8RQJdVkaFXNevD
nonce: 66097883000051
block_hash: DTAXqtkUdpUp1oaPThYVMLYu1QnT2KzQFhisuQ3iCsSp
signer_id: solops2.testnet
receiver_id: demo-devhub-vid102.testnet
actions:
-- function call:
method name: set_greeting
args: {
"greeting": "hello world"
}
gas: 100.0 Tgas
deposit: 0 NEAR

Now that you have the Base64 signed transaction, you're ready to send the transaction to the RPC server, so the transaction gets executed and changes are committed to the blockchain. Let's use the near-cli again, to see how it works:

near --teach-me transaction send-signed-transaction 'DwAAAHNvbG9wczIudGVzdG5ldABWZI8xXoQFnilyGUEmEPxsqxg1hcdnIY2lLY+snNEf6vMs1pwdPAAAGgAAAGRlbW8tZGV2aHViLXZpZDEwMi50ZXN0bmV0uQAfK/w+8BKQJhwJzbP0lq4q217VghNNcGLtlzKkZykBAAAAAgwAAABzZXRfZ3JlZXRpbmcaAAAAeyJncmVldGluZyI6ImhlbGxvIHdvcmxkIn0AQHoQ81oAAAAAAAAAAAAAAAAAAAAAAAAAkUn66wC30xf2CFUjFE8oOlArYiinVPBHau+zTbP8iFWq/xJ9QM9TWpTG9VmWi31H+hQwCEE0lQlDxPNx5gdbDQ==' network-config testnet send

Once you execute the near command, you'll get a low-level detail of the whole communication process to submit the signed transaction:

 INFO Sending transaction ...:Broadcasting transaction via RPC: https://archival-rpc.testnet.near.org/
INFO I am making HTTP call to NEAR JSON RPC to broadcast a transaction, learn more https://docs.near.org/api/rpc/transactions#send-tx
INFO HTTP POST https://archival-rpc.testnet.near.org/
INFO JSON Request Body:
| {
| "id": "he4XxvkEP",
| "jsonrpc": "2.0",
| "method": "broadcast_tx_commit",
| "params": [
| "DwAAAHNvbG9wczIudGVzdG5ldABWZI8xXoQFnilyGUEmEPxsqxg1hcdnIY2lLY+snNEf6vMs1pwdPAAAGgAAAGRlbW8tZGV2aHViLXZpZDEwMi50ZXN0bmV0uQAfK/w+8BKQJhwJzbP0lq4q217VghNNcGLtlzKkZykBAAAAAgwAAABzZXRfZ3JlZXRpbmcaAAAAeyJncmVldGluZyI6ImhlbGxvIHdvcmxkIn0AQHoQ81oAAAAAAAAAAAAAAAAAAAAAAAAAkUn66wC30xf2CFUjFE8oOlArYiinVPBHau+zTbP8iFWq/xJ9QM9TWpTG9VmWi31H+hQwCEE0lQlDxPNx5gdbDQ=="
| ]
| }
INFO JSON RPC Response:
| {
| "receipts_outcome": [
| {
| "block_hash": "E5gQ8DfoAct7xDMsQjkfjU1JM1gWoa1SevrqAPMV1b7R",
| "id": "929rfLGmN1yRVCyk3A9X2bArmxspLHzhGKUhysdNBdBP",
| "outcome": {
| "executor_id": "demo-devhub-vid102.testnet",
| "gas_burnt": 8367821499570,
| "logs": [
| "Guardando el mensaje hello world"
| ],
| "metadata": {
| "gas_profile": [
| {
| "cost": "BASE",
| "cost_category": "WASM_HOST_COST",
| "gas_used": "2382912999"
| },
| {
| "cost": "CONTRACT_LOADING_BASE",
| "cost_category": "WASM_HOST_COST",
| "gas_used": "35445963"
| },
| {
| "cost": "CONTRACT_LOADING_BYTES",
| "cost_category": "WASM_HOST_COST",
| "gas_used": "575869962585"
| },
| {
| "cost": "LOG_BASE",
| "cost_category": "WASM_HOST_COST",
| "gas_used": "3543313050"
| },
| {
| "cost": "LOG_BYTE",
| "cost_category": "WASM_HOST_COST",
| "gas_used": "422361312"
| },
| {
| "cost": "READ_CACHED_TRIE_NODE",
| "cost_category": "WASM_HOST_COST",
| "gas_used": "2280000000"
| },
| {
| "cost": "READ_MEMORY_BASE",
| "cost_category": "WASM_HOST_COST",
| "gas_used": "10439452800"
| },
| {
| "cost": "READ_MEMORY_BYTE",
| "cost_category": "WASM_HOST_COST",
| "gas_used": "258490644"
| },
| {
| "cost": "READ_REGISTER_BASE",
| "cost_category": "WASM_HOST_COST",
| "gas_used": "5034330372"
| },
| {
| "cost": "READ_REGISTER_BYTE",
| "cost_category": "WASM_HOST_COST",
| "gas_used": "5125224"
| },
| {
| "cost": "STORAGE_READ_BASE",
| "cost_category": "WASM_HOST_COST",
| "gas_used": "56356845749"
| },
| {
| "cost": "STORAGE_READ_KEY_BYTE",
| "cost_category": "WASM_HOST_COST",
| "gas_used": "154762665"
| },
| {
| "cost": "STORAGE_READ_VALUE_BYTE",
| "cost_category": "WASM_HOST_COST",
| "gas_used": "145886104"
| },
| {
| "cost": "STORAGE_WRITE_BASE",
| "cost_category": "WASM_HOST_COST",
| "gas_used": "64196736000"
| },
| {
| "cost": "STORAGE_WRITE_EVICTED_BYTE",
| "cost_category": "WASM_HOST_COST",
| "gas_used": "835049982"
| },
| {
| "cost": "STORAGE_WRITE_KEY_BYTE",
| "cost_category": "WASM_HOST_COST",
| "gas_used": "352414335"
| },
| {
| "cost": "STORAGE_WRITE_VALUE_BYTE",
| "cost_category": "WASM_HOST_COST",
| "gas_used": "806482014"
| },
| {
| "cost": "TOUCHING_TRIE_NODE",
| "cost_category": "WASM_HOST_COST",
| "gas_used": "241529338890"
| },
| {
| "cost": "UTF8_DECODING_BASE",
| "cost_category": "WASM_HOST_COST",
| "gas_used": "3111779061"
| },
| {
| "cost": "UTF8_DECODING_BYTE",
| "cost_category": "WASM_HOST_COST",
| "gas_used": "9330575328"
| },
| {
| "cost": "WASM_INSTRUCTION",
| "cost_category": "WASM_HOST_COST",
| "gas_used": "6485096078472"
| },
| {
| "cost": "WRITE_MEMORY_BASE",
| "cost_category": "WASM_HOST_COST",
| "gas_used": "8411384583"
| },
| {
| "cost": "WRITE_MEMORY_BYTE",
| "cost_category": "WASM_HOST_COST",
| "gas_used": "185216496"
| },
| {
| "cost": "WRITE_REGISTER_BASE",
| "cost_category": "WASM_HOST_COST",
| "gas_used": "8596567458"
| },
| {
| "cost": "WRITE_REGISTER_BYTE",
| "cost_category": "WASM_HOST_COST",
| "gas_used": "296521992"
| }
| ],
| "version": 3
| },
| "receipt_ids": [
| "FL27aXAUiKouK2hQTa2x37XSFer1298dRruE9mYGjEKs"
| ],
| "status": {
| "SuccessValue": ""
| },
| "tokens_burnt": "836782149957000000000"
| },
| "proof": [
| {
| "direction": "Right",
| "hash": "5qTrJh67c7kADK83TvmiyX79NDbGRmJrjUq92kJgnSTA"
| }
| ]
| }
| ],
| "status": {
| "SuccessValue": ""
| },
| "transaction": {
| "actions": [
| {
| "FunctionCall": {
| "args": "eyJncmVldGluZyI6ImhlbGxvIHdvcmxkIn0=",
| "deposit": "0",
| "gas": 100000000000000,
| "method_name": "set_greeting"
| }
| }
| ],
| "hash": "GAZgvCCbnbRhZx5gcdmdHL83XFairbJrUfcJxu8NxtR5",
| "nonce": 66097883000051,
| "priority_fee": 0,
| "public_key": "ed25519:6pEzqbRe2XnSPjQ7K7UpApfszDDcbY8RQJdVkaFXNevD",
| "receiver_id": "demo-devhub-vid102.testnet",
| "signature": "ed25519:3uUiouFRtEWLURdq7itnYL8KVNLdKYiua3zv2xxoJpqTNghmgdzMMqL6FxsovyWSbQJqE5oBsMKGcAKBrE2499Tz",
| "signer_id": "solops2.testnet"
| },
| "transaction_outcome": {
| "block_hash": "faxFMiPY6DufbYYdPjy9a2DxrHZ6oqAH5DGNP3pxT3t",
| "id": "GAZgvCCbnbRhZx5gcdmdHL83XFairbJrUfcJxu8NxtR5",
| "outcome": {
| "executor_id": "solops2.testnet",
| "gas_burnt": 309871481170,
| "logs": [],
| "metadata": {
| "gas_profile": null,
| "version": 1
| },
| "receipt_ids": [
| "929rfLGmN1yRVCyk3A9X2bArmxspLHzhGKUhysdNBdBP"
| ],
| "status": {
| "SuccessReceiptId": "929rfLGmN1yRVCyk3A9X2bArmxspLHzhGKUhysdNBdBP"
| },
| "tokens_burnt": "30987148117000000000"
| },
| "proof": [
| {
| "direction": "Right",
| "hash": "Ap3rGc4PQT7NianFgKkuv7z42Js8JPCXyTzH4CH1uw1V"
| },
| {
| "direction": "Left",
| "hash": "GPm934B9erHkih84pAwL15mpUinTABmijwSmFF9Ds7sF"
| },
| {
| "direction": "Right",
| "hash": "EJJvV5MayP5jPGAUsR26cpN9HBsnGynh5VC1zEYh6nva"
| }
| ]
| }
| }

--- Logs ---------------------------
Logs [demo-devhub-vid102.testnet]:
Guardando el mensaje hello world
--- Result -------------------------
Empty result
------------------------------------

The "set_greeting" call to <demo-devhub-vid102.testnet> on behalf of <solops2.testnet> succeeded.

Gas burned: 8.7 Tgas
Transaction fee: 0.000867769298074 NEAR
Transaction ID: GAZgvCCbnbRhZx5gcdmdHL83XFairbJrUfcJxu8NxtR5
To see the transaction in the transaction explorer, please open this url in your browser:
https://explorer.testnet.near.org/transactions/GAZgvCCbnbRhZx5gcdmdHL83XFairbJrUfcJxu8NxtR5

As shown in the log, the CLI sends a JSON message to the RPC server calling the broadcast_tx_commit method, setting the Base64 signed transaction as params. If the transaction was crafted correctly and the signature is valid, the RPC server will reply with the transaction outcome, logs, and receipts.

By following this process, you can leverage the powerful near-cli to act as a guide when you are building your own client library. As long as your code can create the same JSON messages, and handle the responses from the RPC server, you'll be able add support for the blockchain interactions that you need.

As a reference, you might want to add support for other RPC methods listed here:

Your client can support as many methods as you want, depending on the actions that you want to do on the blockchain. For example, if you only want to interact with smart contracts, then the amount of methods to implement are reduced considerably.

What's next​

I hope this proof of concept opens the NEAR ecosystem to developers across new languages and platforms, beyond the crypto-native devs that have been surfing the waves of Web3 since Bitcoin was published.

info

If you're planning on using the cNEAR library and you need methods that are not currently available, please create a GitHub issue and I'll be happy to help improving support and fixing issues.


Resources​

  1. cNEAR library
  2. PS3 Example
  3. Nomicon documentation
  4. RPC API documentation
  5. near-cli