UNPKG

@apexfusionfoundation/blockfrost-js

Version:

A JavaScript/TypeScript SDK for interacting with the https://blockfrost.io API

257 lines (256 loc) 11.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.verifyWebhookSignature = exports.parseAsset = exports.getFingerprint = exports.hexToString = exports.deriveAddress = void 0; const vector_serialization_lib_nodejs_1 = require("@apexfusionfoundation/vector-serialization-lib-nodejs"); const cip14_js_1 = __importDefault(require("@emurgo/cip14-js")); const crypto_1 = require("crypto"); const errors_1 = require("./errors"); /** * Derives an address with derivation path `m/1852'/1815'/account'/role/addressIndex` * If role is set to `2` then it returns a stake address (`m/1852'/1815'/account'/2/addressIndex`) * * @param accountPublicKey - hex-encoded account public key * @param role - role within derivation path `m/1852'/1815'/account'/role/addressIndex` * @param addressIndex - address index within derivation path `m/1852'/1815'/account'/role/addressIndex` * @param isTestnet - Whether to derive testnet address * @param isByron - Whether to derive Byron address (Optional, default false) * @returns Object with bech32 address and corresponding partial derivation path `{address: string, path: [role, addressIndex]}` * @example * * ```ts * const Blockfrost = require('@blockfrost/blockfrost-js'); * const res = Blockfrost.deriveAddress( * '7ec9738746cb4708df52a455b43aa3fdee8955abaf37f68ffc79bb84fbf9e1b39d77e2deb9749faf890ff8326d350ed3fd0e4aa271b35cad063692af87102152', * 0, * 1, * false, * ); * console.log(res); * // { * // address: 'addr1qy535472n2ctu3x55v03zmm9jnz54grqu3sueap9pnk4xys49ucjdfty5p5qlw5qe28v9k988stffc2g0hx2xx86a2dq5u58qk', * // path: [0, 1], * // } * ``` * */ const deriveAddress = (accountPublicKey, role, addressIndex, isTestnet, isByron) => { const accountKey = vector_serialization_lib_nodejs_1.Bip32PublicKey.from_bytes(Buffer.from(accountPublicKey, 'hex')); const utxoPubKey = accountKey.derive(role).derive(addressIndex); const mainStakeKey = accountKey.derive(2).derive(0); const testnetNetworkInfo = vector_serialization_lib_nodejs_1.NetworkInfo.testnet(); const mainnetNetworkInfo = vector_serialization_lib_nodejs_1.NetworkInfo.mainnet(); const networkId = isTestnet ? testnetNetworkInfo.network_id() : mainnetNetworkInfo.network_id(); const utxoPubRawKey = utxoPubKey.to_raw_key(); const utxoPubKeyHash = utxoPubRawKey.hash(); const mainStakeRawKey = mainStakeKey.to_raw_key(); const mainStakeKeyHash = mainStakeRawKey.hash(); const utxoStakeCred = vector_serialization_lib_nodejs_1.StakeCredential.from_keyhash(utxoPubKeyHash); const mainStakeCred = vector_serialization_lib_nodejs_1.StakeCredential.from_keyhash(mainStakeKeyHash); const baseAddr = vector_serialization_lib_nodejs_1.BaseAddress.new(networkId, utxoStakeCred, mainStakeCred); utxoStakeCred.free(); mainStakeCred.free(); mainStakeKeyHash.free(); mainStakeRawKey.free(); utxoPubKeyHash.free(); utxoPubRawKey.free(); const baseAddrBech32 = baseAddr.to_address().to_bech32(); baseAddr.free(); if (role === 2 && !isByron) { const addressSpecificStakeKey = accountKey.derive(2).derive(addressIndex); const stakeRawKey = addressSpecificStakeKey.to_raw_key(); const stakeKeyHash = stakeRawKey.hash(); const stakeCred = vector_serialization_lib_nodejs_1.StakeCredential.from_keyhash(stakeKeyHash); // always return stake address const rewardAddr = vector_serialization_lib_nodejs_1.RewardAddress.new(networkId, stakeCred); const address = rewardAddr.to_address(); const rewardAddrBech32 = address.to_bech32(); address.free(); rewardAddr.free(); addressSpecificStakeKey.free(); stakeKeyHash.free(); stakeRawKey.free(); stakeCred.free(); return { address: rewardAddrBech32, path: [role, addressIndex], }; } if (isByron) { const protocolMagic = isTestnet ? testnetNetworkInfo.protocol_magic() : mainnetNetworkInfo.protocol_magic(); const byronAddress = vector_serialization_lib_nodejs_1.ByronAddress.icarus_from_key(utxoPubKey, protocolMagic); const byronAddrBase58 = byronAddress.to_base58(); byronAddress.free(); return { address: byronAddrBase58, path: [role, addressIndex], }; } mainStakeKey.free(); utxoPubKey.free(); accountKey.free(); testnetNetworkInfo.free(); mainnetNetworkInfo.free(); return { address: baseAddrBech32, path: [role, addressIndex], }; }; exports.deriveAddress = deriveAddress; const hexToString = (input) => { const hex = input.toString(); let str = ''; for (let n = 0; n < hex.length; n += 2) { str += String.fromCharCode(parseInt(hex.substr(n, 2), 16)); } return str; }; exports.hexToString = hexToString; /** * Calculates asset fingerprint. * * @param policyId - Policy Id * @param assetName - hex-encoded asset name * @returns Asset fingerprint for the given policy ID and asset name. * @example * * ```ts * const Blockfrost = require('@blockfrost/blockfrost-js'); * const res = Blockfrost.getFingerprint( * '00000002df633853f6a47465c9496721d2d5b1291b8398016c0e87ae', * '6e7574636f696e', * ); * console.log(res); * // 'asset12h3p5l3nd5y26lr22am7y7ga3vxghkhf57zkhd' * ``` * */ const getFingerprint = (policyId, assetName) => cip14_js_1.default.fromParts(Uint8Array.from(Buffer.from(policyId, 'hex')), Uint8Array.from(Buffer.from(assetName || '', 'hex'))).fingerprint(); exports.getFingerprint = getFingerprint; /** * Parses asset hex and returns its policy ID, asset name and fingerprint. * * @param hex - hex-encoded asset * @returns Object containing `policyId`, `assetName`, `assetNameHex` and `fingerprint`. * @example * * ```ts * const Blockfrost = require('@blockfrost/blockfrost-js'); * const res = Blockfrost.parseAsset('00000002df633853f6a47465c9496721d2d5b1291b8398016c0e87ae6e7574636f696e'); * console.log(res); * // { * // "assetName": 'nutcoin', * // "assetNameHex": '6e7574636f696e', * // "fingerprint": 'asset12h3p5l3nd5y26lr22am7y7ga3vxghkhf57zkhd', * // "policyId": '00000002df633853f6a47465c9496721d2d5b1291b8398016c0e87ae', * // } * ``` * */ const parseAsset = (hex) => { const policyIdSize = 56; const policyId = hex.slice(0, policyIdSize); const assetNameHex = hex.slice(policyIdSize); const assetName = (0, exports.hexToString)(assetNameHex); const fingerprint = (0, exports.getFingerprint)(policyId, assetNameHex); return { policyId, assetName, assetNameHex, fingerprint, }; }; exports.parseAsset = parseAsset; /** * Verifies webhook signature * @remarks * Webhooks enable Blockfrost to push real-time notifications to your application. In order to prevent malicious actor from pretending to be Blockfrost every webhook request is signed. The signature is included in a request's `Blockfrost-Signature` header. This allows you to verify that the events were sent by Blockfrost, not by a third party. * * To learn more about Secure Webhooks, see [Secure Webhooks Docs](https://blockfrost.dev/docs/start-building/webhooks/). * For full example project, see [webhook-basic example](https://github.com/blockfrost/blockfrost-js-examples/tree/master/examples/webhook-basic). * * @param webhookPayload - Buffer or stringified payload of the webhook request. * @param signatureHeader - Buffer or stringified Blockfrost-Signature header. * @param secret - Auth token for the webhook. * @param timestampToleranceSeconds - Time tolerance affecting signature validity. Optional, by default signatures older than 600s are considered invalid. * @returns `true` for the valid signature, otherwise throws `SignatureVerificationError` * * @throws {@link SignatureVerificationError} * Thrown if the signature is not valid. For easier debugging the SignatureVerificationError has additional detail object with 2 properties - header and request_body. * */ const verifyWebhookSignature = (webhookPayload, signatureHeader, secret, timestampToleranceSeconds = 600) => { let timestamp; if (Array.isArray(signatureHeader)) { throw new errors_1.SignatureVerificationError('Unexpected: An array was passed as a Blockfrost-Signature header'); } const decodedWebhookPayload = Buffer.isBuffer(webhookPayload) ? webhookPayload.toString('utf8') : webhookPayload; const decodedSignatureHeader = Buffer.isBuffer(signatureHeader) ? signatureHeader.toString('utf8') : signatureHeader; // Parse signature header (example: t=1648550558,v1=162381a59040c97d9b323cdfec02facdfce0968490ec1732f5d938334c1eed4e,v1=...) const signatures = []; const tokens = decodedSignatureHeader.split(','); for (const token of tokens) { const [key, value] = token.split('='); switch (key) { case 't': timestamp = Number(value); break; case 'v1': signatures.push(value); break; default: console.warn(`Cannot parse part of the signature header, key "${key}" is not supported by this version of Blockfrost SDK.`); } } if (!timestamp || tokens.length < 2) { // timestamp and at least one signature must be present throw new errors_1.SignatureVerificationError('Invalid signature header format.', { signatureHeader: decodedSignatureHeader, webhookPayload: decodedWebhookPayload, }); } if (signatures.length === 0) { throw new errors_1.SignatureVerificationError('No signatures with supported version scheme.', { signatureHeader: decodedSignatureHeader, webhookPayload: decodedWebhookPayload, }); } let hasValidSignature = false; for (const signature of signatures) { // Recreate signature by concatenating timestamp with stringified payload, // then compute HMAC using sha256 and provided secret (auth token) const signaturePayload = `${timestamp}.${decodedWebhookPayload}`; const hmac = (0, crypto_1.createHmac)('sha256', secret) .update(signaturePayload) .digest('hex'); // computed hmac should match signature parsed from a signature header if (hmac === signature) { hasValidSignature = true; } } if (!hasValidSignature) { throw new errors_1.SignatureVerificationError('No signature matches the expected signature for the payload.', { signatureHeader: decodedSignatureHeader, webhookPayload: decodedWebhookPayload, }); } const currentTimestamp = Math.floor(new Date().getTime() / 1000); if (currentTimestamp - timestamp > timestampToleranceSeconds) { // Event is older than timestamp_tolerance_seconds throw new errors_1.SignatureVerificationError("Signature's timestamp is outside of the time tolerance", { signatureHeader: decodedSignatureHeader, webhookPayload: decodedWebhookPayload, }); } else { // Successfully validate the signature only if it is within timestamp_tolerance_seconds tolerance return true; } }; exports.verifyWebhookSignature = verifyWebhookSignature;