import { encodeAddress, randomAsHex } from '@polkadot/util-crypto';
import {
u8aToHex,
u8aToString,
stringToHex,
bufferToU8a,
} from '@polkadot/util';
import { getDidNonce, getStateChange } from '../utils/misc';
import { isHexWithGivenByteSize, getHexIdentifier } from '../utils/codec';
import NoBlobError from '../utils/errors/no-blob-error';
import { typedHexDID, createDidSig } from '../utils/did';
export const DockBlobQualifier = 'blob:dock:';
export const DockBlobIdByteSize = 32;
// Maximum size of the blob in bytes
// implementer may choose to implement this as a dynamic config option settable with the `parameter_type!` macro
export const BLOB_MAX_BYTE_SIZE = 8192;
/**
* Check if the given identifier is 32 byte hex
* @param {string} identifier - The identifier to check.
* @return {void} Throws exception if invalid identifier
*/
export function validateBlobIDHexIdentifier(identifier) {
if (!isHexWithGivenByteSize(identifier, DockBlobIdByteSize)) {
throw new Error(`ID must be ${DockBlobIdByteSize} bytes`);
}
}
/**
* Gets the hexadecimal value of the given ID.
* @param {string} id - The ID can be passed as fully qualified ID like `blob:dock:<SS58 string>` or
* a 32 byte hex string
* @return {string} Returns the hexadecimal representation of the ID.
*/
export function getHexIdentifierFromBlobID(id) {
const hexId = getHexIdentifier(id, DockBlobQualifier, DockBlobIdByteSize);
validateBlobIDHexIdentifier(hexId);
return hexId;
}
/**
* Create and return a fully qualified Dock Blob id, i.e. "blob:dock:<SS58 string>"
* @returns {string} - The Blob id
*/
export function createNewDockBlobId() {
const hexId = randomAsHex(DockBlobIdByteSize);
return blobHexIdToQualified(hexId);
}
/**
* Return a fully qualified Dock Blob id, i.e. "blob:dock:<SS58 string>"
* @param {string} hexId - The hex blob id (without the qualifier)
* @returns {string} - The fully qualified Blob id
*/
export function blobHexIdToQualified(hexId) {
const ss58Id = encodeAddress(hexId);
return `${DockBlobQualifier}${ss58Id}`;
}
/** Class to create and update Blobs on chain. */
class BlobModule {
/**
* Creates a new instance of BlobModule and sets the api
* @constructor
* @param {object} api - PolkadotJS API Reference
* @param signAndSend
*/
constructor(api, signAndSend) {
this.api = api;
this.module = api.tx.blobStore;
this.signAndSend = signAndSend;
}
/**
* Create a signed transaction for adding a new blob
* @param blob
* @param signerDid - Signer of the blob
* @param signingKeyRef - The key id used by the signer. This will be used by the verifier (node) to fetch the public key for verification
* @param nonce - The nonce to be used for sending this transaction. If not provided then `didModule` must be provided.
* @param didModule - Reference to the DID module. If nonce is not provided then the next nonce for the DID is fetched by
* using this
* @returns {Promise<*>}
*/
async createNewTx(
blob,
signerDid,
signingKeyRef,
{ nonce = undefined, didModule = undefined },
) {
const signerHexDid = typedHexDID(this.api, signerDid);
const [addBlob, didSig] = await this.createSignedAddBlob(
blob,
signerHexDid,
signingKeyRef,
{ nonce, didModule },
);
return this.module.new(addBlob, didSig);
}
/**
* Write a new blob on chain.
* @param blob
* @param signerDid - Signer of the blob
* @param signingKeyRef - The key id used by the signer. This will be used by the verifier (node) to fetch the public key for verification
* @param nonce - The nonce to be used for sending this transaction. If not provided then `didModule` must be provided.
* @param didModule - Reference to the DID module. If nonce is not provided then the next nonce for the DID is fetched by
* using this
* @param waitForFinalization
* @param params
* @returns {Promise<*>}
*/
async new(
blob,
signerDid,
signingKeyRef,
{ nonce = undefined, didModule = undefined },
waitForFinalization = true,
params = {},
) {
return this.signAndSend(
await this.createNewTx(blob, signerDid, signingKeyRef, {
nonce,
didModule,
}),
waitForFinalization,
params,
);
}
/**
* Get blob with given id from the chain. Throws if the blob can't be found.
* @param {string} id - Can either be a full blob id like blob:dock:0x... or just the hex identifier
* @returns {Promise<Array>} - A 2-element array where the first is the author and the second is the blob contents.
*/
async get(id) {
const hexId = getHexIdentifierFromBlobID(id);
const resp = await this.api.query.blobStore.blobs(hexId);
if (resp.isNone) {
throw new NoBlobError(id);
}
const respTuple = resp.unwrap();
if (respTuple.length === 2) {
let value = bufferToU8a(respTuple[1]);
// Try to convert the value to a JSON object
try {
const strValue = u8aToString(value);
if (strValue.substring(0, 1) === '{') {
value = JSON.parse(strValue);
}
} catch (e) {
// no-op, just use default Uint8 array value
}
return [typedHexDID(this.api, respTuple[0]), value];
}
throw new Error(`Needed 2 items in response but got${respTuple.length}`);
}
/**
* Create an `AddBlob` struct as expected by node and return along with signature.
* @param blob
* @param {DockDidOrDidMethodKey} signerDid - Signer DID
* @param signingKeyRef - The key id used by the signer. This will be used by the verifier (node) to fetch the public key for verification
* @param nonce - The nonce to be used for sending this transaction. If not provided then `didModule` must be provided.
* @param didModule - Reference to the DID module. If nonce is not provided then the next nonce for the DID is fetched by
* using this
* @returns {Promise}
*/
async createSignedAddBlob(
blob,
signerDid,
signingKeyRef,
{ nonce = undefined, didModule = undefined },
) {
if (!blob.blob) {
throw new Error('Blob must have a value!');
}
// eslint-disable-next-line no-param-reassign
nonce = await getDidNonce(signerDid, nonce, didModule);
const blobObj = {
...blob,
blob: this.getSerializedBlobValue(blob.blob),
};
const addBlob = {
blob: blobObj,
nonce,
};
const serializedAddBlob = this.getSerializedBlob(addBlob);
const signature = signingKeyRef.sign(serializedAddBlob);
const didSig = createDidSig(signerDid, signingKeyRef, signature);
return [addBlob, didSig];
}
getSerializedBlobValue(blobValue) {
if (blobValue instanceof Uint8Array) {
return u8aToHex(blobValue);
} else if (typeof blobValue === 'object') {
return stringToHex(JSON.stringify(blobValue));
} else if (
typeof blobValue === 'string'
&& !isHexWithGivenByteSize(blobValue)
) {
return stringToHex(blobValue);
}
// Assuming `blobValue` is in hex
return blobValue;
}
/**
* Serializes the `Blob` (for signing before sending to the node)
* @param {object} blob - `Blob` as expected by the Substrate node
* @returns {Array} An array of Uint8
*/
getSerializedBlob(blob) {
return getStateChange(this.api, 'AddBlob', blob);
}
}
export default BlobModule;