Add proof-measure client + bump to 1.4.0
Some checks failed
Build and Test / build (16.x) (push) Failing after 45s
Build and Test / build (18.x) (push) Failing after 40s
Publish to NPM Registry / publish (release) Successful in 54s
Build and Test / build (20.x) (push) Failing after 38s

proof-measure is a separate, public, unauthenticated Dragonchain service. Adds:
- UnauthHttpClient: HMAC-free transport mirroring DragonchainClient (timeout,
  agent, redirect refusal).
- ProofMeasureClient: getSecurity / report / health; default base URL
  https://proof-measure.dragonchain.com. Standalone (new ProofMeasureClient())
  and via DragonchainSDK.proofMeasure.
- Proof-measure types (decimals as strings, timestamps as numbers).
- jest tests.
This commit is contained in:
2026-06-04 13:45:09 -04:00
parent a35dc508b0
commit 5a943f45a6
7 changed files with 394 additions and 1 deletions

View File

@@ -145,6 +145,37 @@ await sdk.transactionType.delete('my-type');
const block = await sdk.block.get('block-id');
```
### Proof Measure (public, unauthenticated)
`proof-measure` is a separate, **public, unauthenticated** Dragonchain service
(the measured-immutability / "securedBy" metric) at
`https://proof-measure.dragonchain.com`. It needs no credentials. Decimal fields
are returned as strings (full precision) and timestamps as unix-second numbers.
```typescript
// Via the main SDK (targets the default public endpoint):
const sec = await sdk.proofMeasure.getSecurity('BTC', Math.floor(Date.now() / 1000) - 3600);
console.log(`BTC secured by ${sec.valueUsdFormatted} (${sec.raw.value} ${sec.raw.unit})`);
// Or standalone — no prime credentials needed:
import { ProofMeasureClient } from '@dragonchain-inc/prime-sdk';
const pm = new ProofMeasureClient(); // or new ProofMeasureClient({ baseURL: '...' })
const report = await pm.report({
transactionId: 'tx-123',
primeId: 'my-prime',
blockId: '42',
anchors: [
{ network: 'BTC', txHash: '0x...', timestamp: anchorUnix },
{ network: 'ETH', txHash: '0x...', timestamp: anchorUnix },
],
});
console.log(`Secured by ${report.totalValueUsdFormatted} across ${report.anchors.length} anchors`);
await pm.health();
```
## TypeScript Support
This SDK is written in TypeScript and includes complete type definitions:

View File

@@ -1,6 +1,6 @@
{
"name": "@dragonchain-inc/prime-sdk",
"version": "1.2.0",
"version": "1.4.0",
"description": "Official Dragonchain Prime SDK for Node.js and TypeScript",
"main": "./dist/index.js",
"module": "./dist/index.mjs",

View File

@@ -10,6 +10,7 @@ import { TransactionTypeClient } from './transactionType';
import { ContractClient } from './contract';
import { BlockClient } from './block';
import { SystemClient } from './system';
import { ProofMeasureClient, PROOF_MEASURE_DEFAULT_BASE_URL } from './proofMeasure';
/**
* Main Dragonchain SDK class
@@ -22,6 +23,13 @@ export class DragonchainSDK {
public readonly contract: ContractClient;
public readonly block: BlockClient;
public readonly system: SystemClient;
/**
* Client for the public proof-measure service (measured immutability /
* "securedBy"). It is a separate, unauthenticated service, so this handle
* targets its default public endpoint; for a custom endpoint construct a
* `ProofMeasureClient` directly.
*/
public readonly proofMeasure: ProofMeasureClient;
/**
* Creates a new Dragonchain SDK instance
@@ -53,6 +61,9 @@ export class DragonchainSDK {
this.contract = new ContractClient(this.client);
this.block = new BlockClient(this.client);
this.system = new SystemClient(this.client);
// proof-measure is a separate, unauthenticated public service; default it
// to its public endpoint (pass-through the request timeout).
this.proofMeasure = new ProofMeasureClient({ baseURL: PROOF_MEASURE_DEFAULT_BASE_URL, timeout });
}
/**
@@ -72,6 +83,8 @@ export { TransactionTypeClient } from './transactionType';
export { ContractClient } from './contract';
export { BlockClient } from './block';
export { SystemClient } from './system';
export { ProofMeasureClient, PROOF_MEASURE_DEFAULT_BASE_URL } from './proofMeasure';
export { UnauthHttpClient, UnauthClientConfig } from './unauthHttpClient';
// Default export
export default DragonchainSDK;

69
src/proofMeasure.ts Normal file
View File

@@ -0,0 +1,69 @@
/**
* Client for the Dragonchain proof-measure service — the measured-immutability
* / "securedBy" metric for L1L5 verification chains.
*
* proof-measure is a separate, public, UNauthenticated service (no API keys),
* so this client needs only a base URL. It exposes a network's accumulated
* security as both a raw measure (cumulative hashes / stake-seconds) and a USD
* valuation, plus a per-transaction "securedBy" report over interchain anchors.
*/
import { UnauthHttpClient, UnauthClientConfig } from './unauthHttpClient';
import {
ContentType,
ProofMeasureSecurityResult,
ProofMeasureReportRequest,
ProofMeasureTransactionReport,
ProofMeasureHealthResponse,
} from './types';
/** Public production proof-measure endpoint. */
export const PROOF_MEASURE_DEFAULT_BASE_URL = 'https://proof-measure.dragonchain.com';
export class ProofMeasureClient {
private client: UnauthHttpClient;
/**
* @param clientOrConfig - an existing UnauthHttpClient, a config object, or
* nothing. With no argument (or no baseURL) the public production endpoint is
* used, so `new ProofMeasureClient()` works with zero configuration.
*/
constructor(clientOrConfig?: UnauthHttpClient | UnauthClientConfig) {
if (clientOrConfig instanceof UnauthHttpClient) {
this.client = clientOrConfig;
} else {
this.client = new UnauthHttpClient({
baseURL: clientOrConfig?.baseURL || PROOF_MEASURE_DEFAULT_BASE_URL,
timeout: clientOrConfig?.timeout,
agent: clientOrConfig?.agent,
});
}
}
/**
* Returns the security a public network (network = 'BTC' or 'ETH') has
* accumulated since the given unix timestamp, as a raw measure + USD
* valuation. Omit `since` (or pass <= 0) to use the service default window.
*/
async getSecurity(network: string, since?: number): Promise<ProofMeasureSecurityResult> {
const query = since && since > 0 ? `?since=${since}` : '';
return this.client.get<ProofMeasureSecurityResult>(`/api/v1/security/${network}${query}`);
}
/**
* Computes the per-transaction "securedBy" report for the supplied interchain
* anchors: each anchor's raw + USD security since it was placed, plus totals.
*/
async report(request: ProofMeasureReportRequest): Promise<ProofMeasureTransactionReport> {
return this.client.post<ProofMeasureTransactionReport>(
'/api/v1/report',
ContentType.JSON,
request
);
}
/** Reports service liveness and DB reachability. */
async health(): Promise<ProofMeasureHealthResponse> {
return this.client.get<ProofMeasureHealthResponse>('/api/v1/health');
}
}

View File

@@ -213,3 +213,65 @@ export interface ListResponse {
items: unknown[];
total_count: number;
}
// Proof Measure Types (measured immutability / "securedBy").
// Decimal-valued fields are strings (full precision); timestamps are unix-second numbers.
export interface ProofMeasureRawMeasure {
value: string; // human-scaled mantissa, e.g. "484.44"
unit: string; // e.g. "Zettahashes", "ETH·s"
base: string; // unscaled base amount (hashes or stake-seconds)
}
export interface ProofMeasureSecurityResult {
network: string;
consensus: string; // 'pow' | 'pos'
raw: ProofMeasureRawMeasure;
valueUsd: string;
valueUsdFormatted: string;
label: string;
normalizedScore: string;
since: number; // window/anchor start (unix seconds)
asOf: number; // latest sample time (unix seconds)
}
export interface ProofMeasureReportAnchorInput {
network?: string; // 'BTC' | 'ETH'
chainId?: string; // public-chain numeric id (alternative to network)
txHash: string;
timestamp: number; // anchor time, unix seconds
}
export interface ProofMeasureReportRequest {
transactionId: string;
primeId: string;
blockId: string;
anchors: ProofMeasureReportAnchorInput[];
}
export interface ProofMeasureAnchorSecurity {
network: string;
anchorTimestamp: number;
anchorTxHash: string;
security: ProofMeasureSecurityResult;
}
export interface ProofMeasureHashPower {
value: string;
units: string;
}
export interface ProofMeasureTransactionReport {
transactionId: string;
primeId: string;
blockId: string;
anchors: ProofMeasureAnchorSecurity[];
totalValueUsd: string;
totalValueUsdFormatted: string;
hashPower: ProofMeasureHashPower | null; // null when there are no PoW anchors
totalNormalizedScore: string;
}
export interface ProofMeasureHealthResponse {
status: string;
}

139
src/unauthHttpClient.ts Normal file
View File

@@ -0,0 +1,139 @@
/**
* Minimal HTTP client for public Dragonchain services that require no HMAC
* credentials (e.g. the proof-measure service). Mirrors DragonchainClient's
* body handling, redirect refusal, status checks, and JSON parsing, but sends
* no Authorization/Dragonchain/Timestamp headers.
*/
import * as https from 'https';
import * as http from 'http';
import { URL } from 'url';
export interface UnauthClientConfig {
baseURL: string;
timeout?: number;
/**
* Optional http(s).Agent used for every request. Inject an SSRF-guarded
* agent when baseURL is attacker-influenced. The default (no agent) is
* unguarded — the SSRF policy belongs in the caller, not this library.
*/
agent?: http.Agent | https.Agent;
}
export class UnauthHttpClient {
private readonly baseURL: string;
private readonly timeout: number;
private readonly agent?: http.Agent | https.Agent;
constructor(config: UnauthClientConfig) {
this.baseURL = config.baseURL.replace(/\/$/, ''); // Remove trailing slash
this.timeout = config.timeout || 30000; // Default 30 seconds
this.agent = config.agent;
}
public getEndpoint(): string {
return this.baseURL;
}
private async doRequest<T>(
method: string,
path: string,
contentType: string,
body: unknown
): Promise<T> {
let bodyBuffer: Buffer;
if (body === null || body === undefined) {
bodyBuffer = Buffer.from('');
} else if (Buffer.isBuffer(body)) {
bodyBuffer = body;
} else if (typeof body === 'string') {
bodyBuffer = Buffer.from(body);
} else {
bodyBuffer = Buffer.from(JSON.stringify(body));
if (!contentType) {
contentType = 'application/json';
}
}
const fullURL = `${this.baseURL}${path}`;
const parsedURL = new URL(fullURL);
const isHttps = parsedURL.protocol === 'https:';
const options: https.RequestOptions = {
hostname: parsedURL.hostname,
port: parsedURL.port || (isHttps ? 443 : 80),
path: parsedURL.pathname + parsedURL.search,
method: method.toUpperCase(),
headers: {
...(contentType && { 'Content-Type': contentType }),
Accept: 'application/json',
'Content-Length': bodyBuffer.length,
},
timeout: this.timeout,
...(this.agent && { agent: this.agent }),
};
return new Promise<T>((resolve, reject) => {
const httpModule = isHttps ? https : http;
const req = httpModule.request(options, (res) => {
const chunks: Buffer[] = [];
res.on('data', (chunk: Buffer) => {
chunks.push(chunk);
});
res.on('end', () => {
const responseBody = Buffer.concat(chunks);
// Refuse redirects (would mis-parse the empty body and could be an
// SSRF vector if followed to an internal host).
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400) {
reject(
new Error(
`Unexpected redirect (status ${res.statusCode}) to ${res.headers.location ?? '?'}; refusing to follow`
)
);
return;
}
if (res.statusCode && res.statusCode >= 400) {
const errorMessage = responseBody.toString('utf8').trim();
reject(new Error(`API error (status ${res.statusCode}): ${errorMessage}`));
return;
}
if (responseBody.length > 0) {
try {
resolve(JSON.parse(responseBody.toString('utf8')) as T);
} catch (error) {
reject(new Error(`Failed to parse response: ${(error as Error).message}`));
}
} else {
resolve({} as T);
}
});
});
req.on('error', (error) => {
reject(new Error(`Request failed: ${error.message}`));
});
req.on('timeout', () => {
req.destroy();
reject(new Error(`Request timeout after ${this.timeout}ms`));
});
if (bodyBuffer.length > 0) {
req.write(bodyBuffer);
}
req.end();
});
}
public async get<T>(path: string): Promise<T> {
return this.doRequest<T>('GET', path, '', null);
}
public async post<T>(path: string, contentType: string, body: unknown): Promise<T> {
return this.doRequest<T>('POST', path, contentType, body);
}
}

View File

@@ -0,0 +1,79 @@
import { ProofMeasureClient, PROOF_MEASURE_DEFAULT_BASE_URL } from '../src/proofMeasure';
import { UnauthHttpClient } from '../src/unauthHttpClient';
import { ProofMeasureSecurityResult, ProofMeasureTransactionReport } from '../src/types';
// Build a fake transport that is an instanceof UnauthHttpClient (so the
// ProofMeasureClient constructor uses it directly) with mocked get/post.
function fakeClient(get?: jest.Mock, post?: jest.Mock): UnauthHttpClient {
const c = Object.create(UnauthHttpClient.prototype) as UnauthHttpClient;
(c as unknown as { get: jest.Mock }).get = get ?? jest.fn(async () => ({}));
(c as unknown as { post: jest.Mock }).post = post ?? jest.fn(async () => ({}));
return c;
}
describe('ProofMeasureClient', () => {
it('getSecurity builds the path with since and returns the parsed result', async () => {
const sec: ProofMeasureSecurityResult = {
network: 'BTC',
consensus: 'pow',
raw: { value: '71.63', unit: 'Yottahashes', base: '7.16e25' },
valueUsd: '13928170.26',
valueUsdFormatted: '$13,928,170.26',
label: '$13,928,170.26 energy consumed',
normalizedScore: '0.9234',
since: 1780504894,
asOf: 1780591253,
};
const get = jest.fn(async () => sec);
const client = fakeClient(get);
const res = await new ProofMeasureClient(client).getSecurity('BTC', 1780504894);
expect(get).toHaveBeenCalledWith('/api/v1/security/BTC?since=1780504894');
expect(res.network).toBe('BTC');
expect(res.raw.unit).toBe('Yottahashes');
expect(res.valueUsdFormatted).toBe('$13,928,170.26');
});
it('getSecurity omits the since param when not provided', async () => {
const get = jest.fn(async () => ({}));
await new ProofMeasureClient(fakeClient(get)).getSecurity('ETH');
expect(get).toHaveBeenCalledWith('/api/v1/security/ETH');
});
it('report POSTs the request to /api/v1/report and parses the report', async () => {
const report: ProofMeasureTransactionReport = {
transactionId: 't',
primeId: 'p',
blockId: '42',
anchors: [],
totalValueUsd: '100.00',
totalValueUsdFormatted: '$100.00',
hashPower: { value: '40.06', units: 'Yottahashes' },
totalNormalizedScore: '0.9',
};
const post = jest.fn(async () => report);
const client = fakeClient(undefined, post);
const req = {
transactionId: 't',
primeId: 'p',
blockId: '42',
anchors: [{ network: 'BTC', txHash: '0xabc', timestamp: 1712345678 }],
};
const rep = await new ProofMeasureClient(client).report(req);
expect(post).toHaveBeenCalledWith('/api/v1/report', 'application/json', req);
expect(rep.totalValueUsdFormatted).toBe('$100.00');
expect(rep.hashPower?.units).toBe('Yottahashes');
});
it('health calls /api/v1/health', async () => {
const get = jest.fn(async () => ({ status: 'ok' }));
const res = await new ProofMeasureClient(fakeClient(get)).health();
expect(get).toHaveBeenCalledWith('/api/v1/health');
expect(res.status).toBe('ok');
});
it('defaults to the public endpoint with no config', () => {
const pm = new ProofMeasureClient();
expect(pm).toBeInstanceOf(ProofMeasureClient);
expect(PROOF_MEASURE_DEFAULT_BASE_URL).toBe('https://proof-measure.dragonchain.com');
});
});