Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/hirosystems/stacks-encoding-native-js

Encoding & decoding functions for the Stacks blockchain exposed as a fast native Node.js addon
https://github.com/hirosystems/stacks-encoding-native-js

Last synced: 3 days ago
JSON representation

Encoding & decoding functions for the Stacks blockchain exposed as a fast native Node.js addon

Awesome Lists containing this project

README

        

# stacks-encoding-native-js

[![npm version](https://badge.fury.io/js/stacks-encoding-native-js.svg)](https://www.npmjs.com/package/stacks-encoding-native-js)
[![ci](https://github.com/hirosystems/stacks-encoding-native-js/actions/workflows/ci.yml/badge.svg)](https://github.com/hirosystems/stacks-encoding-native-js/actions/workflows/ci.yml)

`stacks-encoding-native-js` is a Node.js [native addon](https://nodejs.org/api/addons.html) library written in Rust, which provides functions for decoding binary/wire formats used in the Stacks blockchain. Features include Clarity values, transactions, post-conditions, Stacks and Bitcoin addresses, and more.

Various ASM/SIMD optimizations are used in areas which are prone to causing CPU bottlenecks when used in hot paths, e.g. decoding raw Clarity values on the fly.

_This project was bootstrapped by [create-neon](https://www.npmjs.com/package/create-neon)._

### Runtime Support
* Node.js v16+

### Platform Support

* Linux (glibc) x86_64
* Linux (glibc) aarch64
* Linux (musl) x86_64
* Linux (musl) aarch64
* MacOS x86_64
* MacOS aarch64 (Apple Silicon M1)
* Windows x86_64

## Installation and Usage

```shell
npm install stacks-encoding-native-js
```

### Decoding serialized Clarity values
_Example Clarity value:_

Expand

```clar
(tuple
(active true)
(address 'SPA2MZWV9N67TBYVWTE0PSSKMJ2F6YXW7CBE6YPW)
(alias "Alice")
(balance u2000)
(ping (ok 250))
(public_key (some 0x02d4dada83bff981f0cb7ebafcfc6fc7cb5e078b9ee2302a93aae19fb90f872e58))
(tags (list u"contributor" u"og \u{e2ad90}" u"clarity"))
)
```

#### Decode serialized Clarity value to repr string
```ts
import * as assert from 'node:assert';
import { decodeClarityValueToRepr } from 'stacks-encoding-native-js';

// Serialized hex string of the example Clarity value (0x-prefix optional, Buffer / Uint8Array also accepted)
const hex = '0x0c00000007066163746976650307616464726573730516142a7f9b4d4c7d2fdbe69c0b6733a484f37bbc3b05616c6961730d00000005416c6963650762616c616e636501000000000000000000000000000007d00470696e670700000000000000000000000000000000fa0a7075626c69635f6b65790a020000002102d4dada83bff981f0cb7ebafcfc6fc7cb5e078b9ee2302a93aae19fb90f872e5804746167730b000000030e0000000b636f6e7472696275746f720e000000066f6720e2ad900e00000007636c6172697479';

const reprStr = decodeClarityValueToRepr(hex);

assert.strictEqual(
reprStr,
`(tuple (active true) (address 'SPA2MZWV9N67TBYVWTE0PSSKMJ2F6YXW7CBE6YPW) (alias "Alice") (balance u2000) (ping (ok 250)) (public_key (some 0x02d4dada83bff981f0cb7ebafcfc6fc7cb5e078b9ee2302a93aae19fb90f872e58)) (tags (list u"contributor" u"og \\u{e2ad90}" u"clarity")))`
);
```

#### Decode serialized Clarity value to object
```ts
import * as assert from 'node:assert';
import { decodeClarityValue } from 'stacks-encoding-native-js';

// Serialized hex string of the example Clarity value (0x-prefix optional, Buffer / Uint8Array also accepted)
const hex = '0x0c00000007066163746976650307616464726573730516142a7f9b4d4c7d2fdbe69c0b6733a484f37bbc3b05616c6961730d00000005416c6963650762616c616e636501000000000000000000000000000007d00470696e670700000000000000000000000000000000fa0a7075626c69635f6b65790a020000002102d4dada83bff981f0cb7ebafcfc6fc7cb5e078b9ee2302a93aae19fb90f872e5804746167730b000000030e0000000b636f6e7472696275746f720e000000066f6720e2ad900e00000007636c6172697479';

// Decode into JSON object
const decoded = decodeClarityValue(hex);
```

Decoded Clarity value output result:

Expand

```ts
// Result object
assert.deepStrictEqual(decoded, {
repr: `(tuple (active true) (address 'SPA2MZWV9N67TBYVWTE0PSSKMJ2F6YXW7CBE6YPW) (alias "Alice") (balance u2000) (ping (ok 250)) (public_key (some 0x02d4dada83bff981f0cb7ebafcfc6fc7cb5e078b9ee2302a93aae19fb90f872e58)) (tags (list u"contributor" u"og \\u{e2ad90}" u"clarity")))`,
hex: '0x0c00000007066163746976650307616464726573730516142a7f9b4d4c7d2fdbe69c0b6733a484f37bbc3b05616c6961730d00000005416c6963650762616c616e636501000000000000000000000000000007d00470696e670700000000000000000000000000000000fa0a7075626c69635f6b65790a020000002102d4dada83bff981f0cb7ebafcfc6fc7cb5e078b9ee2302a93aae19fb90f872e5804746167730b000000030e0000000b636f6e7472696275746f720e000000066f6720e2ad900e00000007636c6172697479',
type_id: 12,
data: {
active: { repr: 'true', hex: '0x03', type_id: 3, value: true },
address: {
repr: "'SPA2MZWV9N67TBYVWTE0PSSKMJ2F6YXW7CBE6YPW",
hex: '0x0516142a7f9b4d4c7d2fdbe69c0b6733a484f37bbc3b',
type_id: 5,
address_version: 22,
address_hash_bytes: '0x142a7f9b4d4c7d2fdbe69c0b6733a484f37bbc3b',
address: 'SPA2MZWV9N67TBYVWTE0PSSKMJ2F6YXW7CBE6YPW'
},
alias: {
repr: '"Alice"',
hex: '0x0d00000005416c696365',
type_id: 13,
data: 'Alice'
},
balance: {
repr: 'u2000',
hex: '0x01000000000000000000000000000007d0',
type_id: 1,
value: '2000'
},
ping: {
repr: '(ok 250)',
hex: '0x0700000000000000000000000000000000fa',
type_id: 7,
value: {
repr: '250',
hex: '0x00000000000000000000000000000000fa',
type_id: 0,
value: '250'
}
},
public_key: {
repr: '(some 0x02d4dada83bff981f0cb7ebafcfc6fc7cb5e078b9ee2302a93aae19fb90f872e58)',
hex: '0x0a020000002102d4dada83bff981f0cb7ebafcfc6fc7cb5e078b9ee2302a93aae19fb90f872e58',
type_id: 10,
value: {
repr: '0x02d4dada83bff981f0cb7ebafcfc6fc7cb5e078b9ee2302a93aae19fb90f872e58',
hex: '0x020000002102d4dada83bff981f0cb7ebafcfc6fc7cb5e078b9ee2302a93aae19fb90f872e58',
type_id: 2,
buffer: '0x02d4dada83bff981f0cb7ebafcfc6fc7cb5e078b9ee2302a93aae19fb90f872e58'
}
},
tags: {
repr: '(list u"contributor" u"og \\u{e2ad90}" u"clarity")',
hex: '0x0b000000030e0000000b636f6e7472696275746f720e000000066f6720e2ad900e00000007636c6172697479',
type_id: 11,
list: [
{
repr: 'u"contributor"',
hex: '0x0e0000000b636f6e7472696275746f72',
type_id: 14,
data: 'contributor'
},
{
repr: 'u"og \\u{e2ad90}"',
hex: '0x0e000000066f6720e2ad90',
type_id: 14,
data: 'og ⭐'
},
{
repr: 'u"clarity"',
hex: '0x0e00000007636c6172697479',
type_id: 14,
data: 'clarity'
}
]
}
}
});
```

#### Decode serialized Clarity value to typed object

```ts
import * as assert from 'node:assert';
import {
decodeClarityValue,
ClarityTypeID,
ClarityValueTuple,
ClarityValuePrincipalStandard,
ClarityValueStringAscii,
ClarityValueInt,
ClarityValueBoolTrue,
ClarityValueResponseOk,
ClarityValueBuffer,
ClarityValueList,
ClarityValueStringUtf8,
ClarityValueOptionalSome,
ClarityValueUInt,
} from 'stacks-encoding-native-js';

// Serialized hex string of the example Clarity value (0x-prefix optional, Buffer / Uint8Array also accepted)
const hex = '0x0c00000007066163746976650307616464726573730516142a7f9b4d4c7d2fdbe69c0b6733a484f37bbc3b05616c6961730d00000005416c6963650762616c616e636501000000000000000000000000000007d00470696e670700000000000000000000000000000000fa0a7075626c69635f6b65790a020000002102d4dada83bff981f0cb7ebafcfc6fc7cb5e078b9ee2302a93aae19fb90f872e5804746167730b000000030e0000000b636f6e7472696275746f720e000000066f6720e2ad900e00000007636c6172697479';

// Provide a typed view of the decoded value
const decoded = decodeClarityValue;
public_key: ClarityValueOptionalSome;
tags: ClarityValueList;
}>>(hex);

assert.deepStrictEqual(decoded.data.tags.list.map(v => v.data), ['contributor', 'og ⭐', 'clarity']);
```

The annotated types are _not_ automatically checked at runtime, so type checks are needed for error handling:

Expand

```ts
assert.strictEqual(decoded.type_id, ClarityTypeID.Tuple);
assert.strictEqual(decoded.data.active.type_id, ClarityTypeID.BoolTrue);
assert.strictEqual(decoded.data.address.type_id, ClarityTypeID.PrincipalStandard);
assert.strictEqual(decoded.data.alias.type_id, ClarityTypeID.StringAscii);
assert.strictEqual(decoded.data.balance.type_id, ClarityTypeID.UInt);
assert.strictEqual(decoded.data.ping.type_id, ClarityTypeID.ResponseOk);
assert.strictEqual(decoded.data.ping.value.type_id, ClarityTypeID.Int);
assert.strictEqual(decoded.data.public_key.type_id, ClarityTypeID.OptionalSome);
assert.strictEqual(decoded.data.public_key.value.type_id, ClarityTypeID.Buffer);
assert.strictEqual(decoded.data.tags.type_id, ClarityTypeID.List);
assert.strictEqual(decoded.data.tags.list[0].type_id, ClarityTypeID.StringUtf8);

// Now we can safely access typed properties
assert.strictEqual(decoded.data.active.value, true);
assert.strictEqual(decoded.data.address.address, 'SPA2MZWV9N67TBYVWTE0PSSKMJ2F6YXW7CBE6YPW');
assert.strictEqual(decoded.data.alias.data, 'Alice');
assert.strictEqual(decoded.data.balance.value, '2000');
assert.strictEqual(decoded.data.ping.value.value, '250');
assert.strictEqual(decoded.data.public_key.value.buffer, '0x02d4dada83bff981f0cb7ebafcfc6fc7cb5e078b9ee2302a93aae19fb90f872e58');
assert.deepStrictEqual(decoded.data.tags.list.map(v => v.data), ['contributor', 'og ⭐', 'clarity']);
```

### Decoding principals

#### Principal from serialized Clarity value

```ts
import * as assert from 'node:assert';
import { decodeClarityValueToPrincipal } from 'stacks-encoding-native-js';

// Serialized hex string of an example Clarity value (0x-prefix optional, Buffer / Uint8Array also accepted)
const standardPrincipal = decodeClarityValueToPrincipal('0x0516a13dce8114be0f707f94470a2e5e86eb402f2923');
assert.strictEqual(principal, 'SP2GKVKM12JZ0YW3ZJH3GMBJYGVNM0BS94ERA45AM');

const contractPrincipal = decodeClarityValueToPrincipal('0x0616a6a7a70f41adbe8eae708ed7ec2cbf41a272182014626974636f696e2d6d6f6e6b6579732d6c616273');
assert.strictEqual(contractPrincipal, 'SP2KAF9RF86PVX3NEE27DFV1CQX0T4WGR41X3S45C.bitcoin-monkeys-labs');
```

#### Stacks address from parts

```ts
import * as assert from 'node:assert';
import { stacksAddressFromParts } from 'stacks-encoding-native-js';

const stacksAddressData = {
version: 26,
// Serialized hex string (0x-prefix optional, Buffer / Uint8Array also accepted)
hash160: '0xcd1f5bc9aa49e7417cee3e5dba1a92567da41af6'
};

const stacksAddress = stacksAddressFromParts(stacksAddressData.version, stacksAddressData.hash160);
assert.strictEqual(stacksAddress, 'ST36HYPY9N94YEGBWXRZ5VEGTJ9B7V90TYTM9HGTJ');
```

#### Stacks address to parts

```ts
import * as assert from 'node:assert';
import { decodeStacksAddress } from 'stacks-encoding-native-js';

const [version, hash160] = decodeStacksAddress('ST36HYPY9N94YEGBWXRZ5VEGTJ9B7V90TYTM9HGTJ');
assert.strictEqual(version, 26);
assert.strictEqual(hash160, '0xcd1f5bc9aa49e7417cee3e5dba1a92567da41af6');
```

### Decoding transactions

```ts
import * as assert from 'node:assert';
import {
decodeTransaction,
AnchorModeID,
TransactionVersion,
TxPayloadTypeID,
TxSpendingConditionSingleSigHashMode,
TxPublicKeyEncoding,
ClarityTypeID,
PostConditionModeID,
PostConditionAuthFlag,
PostConditionAssetInfoID,
PostConditionNonfungibleConditionCodeID,
PostConditionNonFungibleConditionName
} from 'stacks-encoding-native-js';

// Serialized hex string (0x-prefix optional, Buffer / Uint8Array also accepted)
const rawTx = '0000000001040089f5fd1f719e4449c980de38e3504be6770a2698000000000000014500000000000001f400008b510c9e20dc22040953d9d7eabf2038008fa4d89a5a6cb78bb9d513e75cd0df3924af9ce3b5f185705bc2f6ba3071710ec6a8803ed6da4addc40a05a01ee0f503020000000102021689f5fd1f719e4449c980de38e3504be6770a269816a6a7a70f41adbe8eae708ed7ec2cbf41a272182014626974636f696e2d6d6f6e6b6579732d6c61627314626974636f696e2d6d6f6e6b6579732d6c61627301000000000000000000000000000008ba1002162bcf9762d5b90bc36dc1b4759b1727690f92ddd30e6d61726b6574706c6163652d76340a6c6973742d6173736574000000040616a6a7a70f41adbe8eae708ed7ec2cbf41a272182014626974636f696e2d6d6f6e6b6579732d6c61627301000000000000000000000000000008ba010000000000000000000000000c84588001000000000000000000000000000000c8';

const decoded = decodeTransaction(rawTx);
```

Decoded transaction output result:

Expand

```ts
assert.deepStrictEqual(decoded, {
tx_id: "0x49bcdba540d5c486f6f7a71de639a128a65f0378d7571e20c75a61661dd5b469",
chain_id: 1,
version: TransactionVersion.Mainnet,
anchor_mode: AnchorModeID.Any,
auth: {
type_id: PostConditionAuthFlag.Standard,
origin_condition: {
hash_mode: TxSpendingConditionSingleSigHashMode.P2PKH,
key_encoding: TxPublicKeyEncoding.Compressed,
nonce: "325",
signature: "0x008b510c9e20dc22040953d9d7eabf2038008fa4d89a5a6cb78bb9d513e75cd0df3924af9ce3b5f185705bc2f6ba3071710ec6a8803ed6da4addc40a05a01ee0f5",
signer: {
address: "SP24ZBZ8ZE6F48JE9G3F3HRTG9FK7E2H6K2QZ3Q1K",
address_hash_bytes: "0x89f5fd1f719e4449c980de38e3504be6770a2698",
address_version: 22
},
tx_fee: "500"
}
},
payload: {
type_id: TxPayloadTypeID.ContractCall,
address: "SPNWZ5V2TPWGQGVDR6T7B6RQ4XMGZ4PXTEE0VQ0S",
address_hash_bytes: "0x2bcf9762d5b90bc36dc1b4759b1727690f92ddd3",
address_version: 22,
contract_name: "marketplace-v4",
function_name: "list-asset",
function_args: [
{
type_id: ClarityTypeID.PrincipalContract,
hex: "0x0616a6a7a70f41adbe8eae708ed7ec2cbf41a272182014626974636f696e2d6d6f6e6b6579732d6c616273",
repr: "'SP2KAF9RF86PVX3NEE27DFV1CQX0T4WGR41X3S45C.bitcoin-monkeys-labs"
},
{
type_id: ClarityTypeID.UInt,
hex: "0x01000000000000000000000000000008ba",
repr: "u2234"
},
{
type_id: ClarityTypeID.UInt,
hex: "0x010000000000000000000000000c845880",
repr: "u210000000"
},
{
type_id: ClarityTypeID.UInt,
hex: "0x01000000000000000000000000000000c8",
repr: "u200"
}
],
function_args_buffer: "0x000000040616a6a7a70f41adbe8eae708ed7ec2cbf41a272182014626974636f696e2d6d6f6e6b6579732d6c61627301000000000000000000000000000008ba010000000000000000000000000c84588001000000000000000000000000000000c8"
},
post_condition_mode: PostConditionModeID.Deny,
post_conditions: [
{
condition_code: PostConditionNonfungibleConditionCodeID.Sent,
condition_name: PostConditionNonFungibleConditionName.Sent,
asset: {
asset_name: "bitcoin-monkeys-labs",
contract_address: "SP2KAF9RF86PVX3NEE27DFV1CQX0T4WGR41X3S45C",
contract_name: "bitcoin-monkeys-labs"
},
asset_info_id: PostConditionAssetInfoID.NonfungibleAsset,
asset_value: {
hex: "0x01000000000000000000000000000008ba",
repr: "u2234",
type_id: ClarityTypeID.UInt
},
principal: {
address: "SP24ZBZ8ZE6F48JE9G3F3HRTG9FK7E2H6K2QZ3Q1K",
address_hash_bytes: "0x89f5fd1f719e4449c980de38e3504be6770a2698",
address_version: 22,
type_id: ClarityTypeID.Buffer
}
}
],
post_conditions_buffer: "0x020000000102021689f5fd1f719e4449c980de38e3504be6770a269816a6a7a70f41adbe8eae708ed7ec2cbf41a272182014626974636f696e2d6d6f6e6b6579732d6c61627314626974636f696e2d6d6f6e6b6579732d6c61627301000000000000000000000000000008ba10"
});
```

## Project Layout

The directory structure of this project is:


/
├── Cargo.toml # The Cargo manifest file
├── package.json # The npm manifest file
├── native/ # The Node addon modules built by this project, these are dynamically-linked shared objects
├── src/**/*.rs # Directory containing the Rust source code for the project
|── index.ts # Typescript definitions for the js interface exposed by the Node addon
|── loader.js # Script to determine which addon file to load based on the executing target platform
|── loader.d.ts # Type defintions for the functions exported by the Node addon
|── builder.js # Script to build the native Node addon for the executing host platform
├── tests/*.ts # Js/ts unit tests, primarily testing the Neon (rust<->nodejs) interop layer
└── perf-tests/ # Scripts to run performance benchmarks used by commands in package.json

The Rust source code inside the `src/**/deserialize.rs` files are responsible for deserializing the Stacks blockchain wire/binary formats defined in [SIP-005](https://github.com/stacksgov/sips/blob/main/sips/sip-005/sip-005-blocks-and-transactions.md).

## NPM Library Bundling

The Node addon modules for all supported platforms are compiled by [CI](.github/workflows/build.yml) and bundled inside the npm package. The native binary files are small enough that the bundled npm package is an acceptable ~20 MB in size.