Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions packages/js-dapi-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,35 @@ hosted on Dash masternodes.
npm install @dashevo/dapi-client
```

### Browser usage

Response objects expose byte-valued fields as `Uint8Array` (not Node `Buffer`).
You can convert via `DAPIClient.bytes`:

```javascript
const DAPIClient = require('@dashevo/dapi-client');
const { bytesToHex, hexToBytes } = DAPIClient.bytes;

const hex = bytesToHex(proof.getQuorumHash());
```

A small number of internal code paths still construct `Buffer` instances —
notably `BlockHeadersProvider` (uses `dashcore-lib.BlockHeader` for SPV
parsing) and the `Identifier` constructor inside wasm-dpp. Node has `Buffer` built in;
browser bundlers (Vite, esbuild, webpack 5) typically auto-shim it when the
`buffer` package is installed, or you can polyfill explicitly:

```javascript
import { Buffer } from 'buffer';
globalThis.Buffer = Buffer;
```

This requirement will go away once
[dashpay/dashcore-lib#315](https://github.com/dashpay/dashcore-lib/pull/315)
(widening `BufferReader` to accept `Uint8Array`) lands and is picked up here.
Until then, browser consumers must ensure a `Buffer` global is reachable at
runtime.

## Usage

### Basic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const SimplifiedMNListDiff = require('@dashevo/dashcore-lib/lib/deterministicmnl
const cbor = require('cbor');

const logger = require('../logger');
const { bytesToHex } = require('../utils/bytes');

class SimplifiedMasternodeListProvider {
/**
Expand Down Expand Up @@ -100,25 +101,31 @@ class SimplifiedMasternodeListProvider {

let simplifiedMNListDiff;
let simplifiedMNListDiffObject;
let simplifiedMNListDiffBuffer;
let simplifiedMNListDiffBytes;
try {
simplifiedMNListDiffBuffer = Buffer.from(response.getMasternodeListDiff_asU8());
simplifiedMNListDiffBytes = new Uint8Array(response.getMasternodeListDiff_asU8());

simplifiedMNListDiffObject = cbor.decodeFirstSync(simplifiedMNListDiffBuffer);
simplifiedMNListDiffObject = cbor.decodeFirstSync(simplifiedMNListDiffBytes);

simplifiedMNListDiff = new SimplifiedMNListDiff(
simplifiedMNListDiffObject,
this.options.network,
);
} catch (e) {
// Guard against bytesToHex throwing (and masking the real error)
// if simplifiedMNListDiffBytes is undefined because the failure
// happened before line 106 assigned it.
const diffBytesHex = simplifiedMNListDiffBytes instanceof Uint8Array
? bytesToHex(simplifiedMNListDiffBytes)
: null;
this.logger.warn(
`Can't parse masternode list diff: ${e.message}`,
{
diffCount,
network: this.options.network,
error: e,
simplifiedMNListDiffObject,
simplifiedMNListDiffBytes: simplifiedMNListDiffBuffer.toString('hex'),
simplifiedMNListDiffBytes: diffBytesHex,
},
);

Expand Down
2 changes: 2 additions & 0 deletions packages/js-dapi-client/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ const DAPIClient = require('./DAPIClient');

const NotFoundError = require('./transport/GrpcTransport/errors/NotFoundError');
const BlockHeadersProvider = require('./BlockHeadersProvider/BlockHeadersProvider');
const bytes = require('./utils/bytes');

DAPIClient.Errors = {
NotFoundError,
};

DAPIClient.BlockHeadersProvider = BlockHeadersProvider;
DAPIClient.bytes = bytes;

module.exports = DAPIClient;
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function getBlockByHashFactory(grpcTransport) {
* @typedef {getBlockByHash}
* @param {string} hash
* @param {DAPIClientOptions} [options]
* @returns {Promise<null|Buffer>}
* @returns {Promise<null|Uint8Array>}
*/
async function getBlockByHash(hash, options = {}) {
const getBlockRequest = new GetBlockRequest();
Expand All @@ -29,7 +29,7 @@ function getBlockByHashFactory(grpcTransport) {
);
const blockBinaryArray = response.getBlock();

return Buffer.from(blockBinaryArray);
return new Uint8Array(blockBinaryArray);
Comment on lines 30 to +32
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: new Uint8Array(response.getBlock()) corrupts the block payload under grpc-web — use getBlock_asU8()

This factory was modified by this PR (e703e90 changed Buffer.from(response.getBlock()) to new Uint8Array(response.getBlock())). The google-protobuf JS binding returns bytes fields as a Uint8Array under grpc-js but as a base64 string under grpc-web. new Uint8Array(string) does NOT base64-decode — it coerces the string length to a numeric and produces a zero-length / bogus typed array, silently corrupting the block bytes returned to callers. The latest delta in this PR explicitly fixed this same pattern in GetTransactionResponse, GetIdentityResponse, GetIdentityByPublicKeyHashResponse, GetStatusResponse, Proof, and WaitForStateTransitionResultResponse using the _asU8() accessor for exactly this reason. Apply the same fix here; the generated bindings expose getBlock_asU8() for this purpose.

Suggested change
const blockBinaryArray = response.getBlock();
return Buffer.from(blockBinaryArray);
return new Uint8Array(blockBinaryArray);
return response.getBlock_asU8();

source: ['codex']

}

return getBlockByHash;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function getBlockByHeightFactory(grpcTransport) {
* @typedef {getBlockByHeight}
* @param {number} height
* @param {DAPIClientOptions} [options]
* @returns {Promise<null|Buffer>}
* @returns {Promise<null|Uint8Array>}
*/
async function getBlockByHeight(height, options = {}) {
const getBlockRequest = new GetBlockRequest();
Expand All @@ -30,7 +30,7 @@ function getBlockByHeightFactory(grpcTransport) {

const blockBinaryArray = response.getBlock();

return Buffer.from(blockBinaryArray);
return new Uint8Array(blockBinaryArray);
Comment on lines 31 to +33
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: new Uint8Array(response.getBlock()) corrupts the block payload under grpc-web — use getBlock_asU8()

Same root cause as getBlockByHashFactory.js: this PR (e703e90) replaced Buffer.from(response.getBlock()) with new Uint8Array(response.getBlock()). Under grpc-web response.getBlock() can be a base64 string, and new Uint8Array(string) does not decode the payload — it yields garbage bytes. The PR already standardized on the _asU8() accessor in six sibling response classes; the same fix applies here. The generated GetBlockResponse exposes getBlock_asU8().

Suggested change
const blockBinaryArray = response.getBlock();
return Buffer.from(blockBinaryArray);
return new Uint8Array(blockBinaryArray);
return response.getBlock_asU8();

source: ['codex']

}

return getBlockByHeight;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,18 @@ function getBlockchainStatusFactory(grpcTransport) {

const responseObject = response.toObject();

// Respond with Buffers instead of base64 for binary fields
// Respond with Uint8Arrays instead of base64 for binary fields

if (response.getChain()) {
if (response.getChain()
.getBestBlockHash()) {
responseObject.chain.bestBlockHash = Buffer.from(response.getChain()
responseObject.chain.bestBlockHash = new Uint8Array(response.getChain()
.getBestBlockHash());
}

if (response.getChain()
.getChainWork()) {
responseObject.chain.chainWork = Buffer.from(response.getChain()
responseObject.chain.chainWork = new Uint8Array(response.getChain()
.getChainWork());
}
}
Comment on lines 34 to 46
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: new Uint8Array(response.getChain().getBestBlockHash() / getChainWork()) corrupts bytes under grpc-web — use _asU8()

This factory was modified by this PR (e703e90 changed Buffer.from(...) to new Uint8Array(...) for bestBlockHash and chainWork). Under grpc-web the protobuf bytes getters return a base64 string rather than a Uint8Array, and new Uint8Array(string) does not base64-decode — it produces a zero-length or NaN-length typed array, silently corrupting the values. The latest delta in this PR (7885bb3) explicitly fixed this exact pattern in six sibling response classes with the comment "new Uint8Array(string) does NOT base64-decode, silently losing bytes." The GetBlockchainStatusResponse chain message exposes getBestBlockHash_asU8() and getChainWork_asU8(); this site was missed in the fix-up sweep.

Suggested change
if (response.getChain()) {
if (response.getChain()
.getBestBlockHash()) {
responseObject.chain.bestBlockHash = Buffer.from(response.getChain()
responseObject.chain.bestBlockHash = new Uint8Array(response.getChain()
.getBestBlockHash());
}
if (response.getChain()
.getChainWork()) {
responseObject.chain.chainWork = Buffer.from(response.getChain()
responseObject.chain.chainWork = new Uint8Array(response.getChain()
.getChainWork());
}
}
if (response.getChain()) {
if (response.getChain()
.getBestBlockHash()) {
// Use _asU8 so we get bytes regardless of the underlying protobuf
// representation (grpc-js: Uint8Array; grpc-web: base64 string).
// new Uint8Array(string) does NOT base64-decode, silently losing bytes.
responseObject.chain.bestBlockHash = response.getChain()
.getBestBlockHash_asU8();
}
if (response.getChain()
.getChainWork()) {
responseObject.chain.chainWork = response.getChain()
.getChainWork_asU8();
}
}

source: ['claude', 'codex']

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const {
CorePromiseClient,
},
} = require('@dashevo/dapi-grpc');
const { base64ToBytes } = require('../../utils/bytes');

/**
* @param {GrpcTransport} grpcTransport
Expand Down Expand Up @@ -34,7 +35,7 @@ function getMasternodeStatusFactory(grpcTransport) {
responseObject.status = Object.keys(GetMasternodeStatusResponse.Status)
.find((key) => GetMasternodeStatusResponse.Status[key] === responseObject.status);

responseObject.proTxHash = Buffer.from(responseObject.proTxHash, 'base64');
responseObject.proTxHash = base64ToBytes(responseObject.proTxHash);

return responseObject;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ class GetTransactionResponse {
/**
*
* @param {object} properties
* @param {Buffer} properties.transaction
* @param {Buffer} properties.blockHash
* @param {Uint8Array} properties.transaction
* @param {Uint8Array} properties.blockHash
* @param {number} properties.height
* @param {number} properties.confirmations
* @param {boolean} properties.isInstantLocked
Expand All @@ -22,15 +22,15 @@ class GetTransactionResponse {

/**
* Get transaction
* @returns {Buffer}
* @returns {Uint8Array}
*/
getTransaction() {
return this.transaction;
}

/**
* Get block hash
* @returns {Buffer}
* @returns {Uint8Array}
*/
getBlockHash() {
return this.blockHash;
Expand Down Expand Up @@ -75,8 +75,8 @@ class GetTransactionResponse {
}

return new GetTransactionResponse({
transaction: Buffer.from(transactionBinaryArray),
blockHash: Buffer.from(proto.getBlockHash()),
transaction: new Uint8Array(transactionBinaryArray),
blockHash: new Uint8Array(proto.getBlockHash()),
height: proto.getHeight(),
confirmations: proto.getConfirmations(),
isInstantLocked: proto.getIsInstantLocked(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const {
} = require('@dashevo/dapi-grpc');

const DAPIClientError = require('../../errors/DAPIClientError');
const { hexToBytes } = require('../../utils/bytes');

/**
* @param {GrpcTransport} grpcTransport
Expand Down Expand Up @@ -41,7 +42,7 @@ function subscribeToBlockHeadersWithChainLocksFactory(grpcTransport) {

if (options.fromBlockHash) {
request.setFromBlockHash(
Buffer.from(options.fromBlockHash, 'hex'),
hexToBytes(options.fromBlockHash),
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const {
} = require('@dashevo/dapi-grpc');

const DAPIClientError = require('../../errors/DAPIClientError');
const { hexToBytes } = require('../../utils/bytes');

/**
* @param {GrpcTransport} grpcTransport
Expand Down Expand Up @@ -65,7 +66,7 @@ function subscribeToTransactionsWithProofsFactory(grpcTransport) {

if (options.fromBlockHash) {
request.setFromBlockHash(
Buffer.from(options.fromBlockHash, 'hex'),
hexToBytes(options.fromBlockHash),
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const InvalidResponseError = require('../response/errors/InvalidResponseError');

class GetDataContractResponse extends AbstractResponse {
/**
* @param {Buffer} dataContract
* @param {Uint8Array} dataContract
* @param {Metadata} metadata
* @param {Proof} [proof]
*/
Expand All @@ -14,7 +14,7 @@ class GetDataContractResponse extends AbstractResponse {
}

/**
* @returns {Buffer}
* @returns {Uint8Array}
*/
getDataContract() {
return this.dataContract;
Expand All @@ -33,7 +33,7 @@ class GetDataContractResponse extends AbstractResponse {
}

return new GetDataContractResponse(
Buffer.from(dataContract),
new Uint8Array(dataContract),
metadata,
Comment thread
PastaPastaPasta marked this conversation as resolved.
proof,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,20 @@ function getDataContractFactory(grpcTransport) {
/**
* Fetch Data Contract by id
* @typedef {getDataContract}
* @param {Buffer} contractId
* @param {Uint8Array} contractId
* @param {DAPIClientOptions & {prove: boolean}} [options]
* @returns {Promise<GetDataContractResponse>}
*/
async function getDataContract(contractId, options = {}) {
const { GetDataContractRequestV0 } = GetDataContractRequest;
const getDataContractRequest = new GetDataContractRequest();

// need to convert objects inherited from Buffer to pure buffer as google protobuf
// need to convert objects inherited from Uint8Array to pure Uint8Array as google protobuf
// doesn't support extended buffers
// https://github.com/protocolbuffers/protobuf/blob/master/js/binary/utils.js#L1049
if (Buffer.isBuffer(contractId)) {
if (contractId instanceof Uint8Array) {
// eslint-disable-next-line no-param-reassign
contractId = Buffer.from(contractId);
contractId = new Uint8Array(contractId);
}

getDataContractRequest.setV0(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function getDataContractHistoryFactory(grpcTransport) {
/**
* Fetch Data Contract by id
* @typedef {getDataContractHistory}
* @param {Buffer} contractId
* @param {Uint8Array} contractId
* @param {bigint} [startAtMs]
* @param {number} [limit]
* @param {number} [offset]
Expand All @@ -34,12 +34,12 @@ function getDataContractHistoryFactory(grpcTransport) {
const { GetDataContractHistoryRequestV0 } = GetDataContractHistoryRequest;
const getDataContractHistoryRequest = new GetDataContractHistoryRequest();

// need to convert objects inherited from Buffer to pure buffer as google protobuf
// need to convert objects inherited from Uint8Array to pure Uint8Array as google protobuf
// doesn't support extended buffers
// https://github.com/protocolbuffers/protobuf/blob/master/js/binary/utils.js#L1049
if (Buffer.isBuffer(contractId)) {
if (contractId instanceof Uint8Array) {
// eslint-disable-next-line no-param-reassign
contractId = Buffer.from(contractId);
contractId = new Uint8Array(contractId);
}

getDataContractHistoryRequest.setV0(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const AbstractResponse = require('../response/AbstractResponse');

class GetDocumentsResponse extends AbstractResponse {
/**
* @param {Buffer[]} documents
* @param {Uint8Array[]} documents
* @param {Metadata} metadata
* @param {Proof} [proof]
*/
Expand All @@ -13,7 +13,7 @@ class GetDocumentsResponse extends AbstractResponse {
}

/**
* @returns {Buffer[]}
* @returns {Uint8Array[]}
*/
getDocuments() {
return this.documents;
Expand All @@ -30,7 +30,7 @@ class GetDocumentsResponse extends AbstractResponse {

return new GetDocumentsResponse(
documents !== undefined
? documents.getDocumentsList().map((document) => Buffer.from(document)) : [],
? documents.getDocumentsList().map((document) => new Uint8Array(document)) : [],
metadata,
proof,
);
Comment on lines 31 to 36
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: getDocumentsList().map((d) => new Uint8Array(d)) corrupts each document under grpc-web — use getDocumentsList_asU8()

This response class was modified by this PR (e703e90). Each entry returned by documents.getDocumentsList() is a protobuf bytes value, which under grpc-web can be a base64 string. Wrapping each entry in new Uint8Array(string) does not base64-decode it; it produces garbage bytes for every document returned. The PR's latest delta already standardized on _asU8() accessors elsewhere for exactly this reason. The generated bindings expose getDocumentsList_asU8() which returns an array of Uint8Array regardless of grpc-js vs grpc-web representation.

Suggested change
return new GetDocumentsResponse(
documents !== undefined
? documents.getDocumentsList().map((document) => Buffer.from(document)) : [],
? documents.getDocumentsList().map((document) => new Uint8Array(document)) : [],
metadata,
proof,
);
return new GetDocumentsResponse(
documents !== undefined
? documents.getDocumentsList_asU8() : [],
metadata,
proof,
);

source: ['codex']

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function getDocumentsFactory(grpcTransport) {
/**
* Fetch Documents from Drive
* @typedef {getDocuments}
* @param {Buffer} contractId - Data Contract ID
* @param {Uint8Array} contractId - Data Contract ID
* @param {string} type - Document type
* @param {DAPIClientOptions & getDocumentsOptions & {prove: boolean}} [options]
* @returns {Promise<GetDocumentsResponse>}
Expand All @@ -44,15 +44,16 @@ function getDocumentsFactory(grpcTransport) {

const { GetDocumentsRequestV0 } = GetDocumentsRequest;
const getDocumentsRequest = new GetDocumentsRequest();
// need to convert Identifier to pure buffer as google protobuf doesn't support extended buffers
// need to convert Identifier to pure Uint8Array as google protobuf doesn't support
// extended buffers
// https://github.com/protocolbuffers/protobuf/blob/master/js/binary/utils.js#L1049

// need to convert objects inherited from Buffer to pure buffer as google protobuf
// need to convert objects inherited from Uint8Array to pure Uint8Array as google protobuf
// doesn't support extended buffers
// https://github.com/protocolbuffers/protobuf/blob/master/js/binary/utils.js#L1049
if (Buffer.isBuffer(contractId)) {
if (contractId instanceof Uint8Array) {
// eslint-disable-next-line no-param-reassign
contractId = Buffer.from(contractId);
contractId = new Uint8Array(contractId);
}

getDocumentsRequest.setV0(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class GetIdentitiesContractKeysResponse extends AbstractResponse {
const keysEntries = identitiesKeys.getEntriesList();

identitiesKeysMap = keysEntries.reduce((acc, entry) => {
const identityId = Identifier.from(Buffer.from(entry.getIdentityId())).toString();
const identityId = Identifier.from(entry.getIdentityId()).toString();
if (!acc[identityId]) {
acc[identityId] = {};
}
Expand Down
Loading
Loading