Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

65 changes: 63 additions & 2 deletions packages/js-evo-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Evo SDK provides a high-level, strongly-typed interface for interacting with [Da
- [Install](#install)
- [Usage](#usage)
- [Facades](#facades)
- [Ranked queries](#ranked-queries)
- [Document references (`refersTo`)](#document-references-refersto)
- [Contributing](#contributing)
- [License](#license)

Expand Down Expand Up @@ -69,10 +71,12 @@ const local = EvoSDK.devnet('paloma', {
await local.connect();
```

Two static helpers are also exported:
Static helpers are also exported:

- `await EvoSDK.setLogLevel(filter)` — configure the underlying Wasm SDK's tracing globally.
- `await EvoSDK.getLatestVersionNumber()` — return the latest Platform protocol version supported by the bundled Wasm SDK.
- `await EvoSDK.maxRankedLimit()` — the hard ceiling on a [ranked / having-range](#ranked-queries) `limit`.
- `await EvoSDK.rankedAverageScale()` — the fixed-point divisor for the `avg` axis of a ranked / having-range result.

## Facades

Expand All @@ -82,7 +86,7 @@ The SDK organises its API into domain-specific facades, each accessible as a pro
|--------|-------------|
| [`sdk.addresses`](src/addresses/facade.ts) | Query balances, transfer credits, withdraw to L1 |
| [`sdk.identities`](src/identities/facade.ts) | Fetch, create, update, and top up identities |
| [`sdk.documents`](src/documents/facade.ts) | Query, create, replace, delete, and transfer documents; aggregate `count` / `sum` / `average` over indexed fields |
| [`sdk.documents`](src/documents/facade.ts) | Query, create, replace, delete, and transfer documents; aggregate `count` / `sum` / `average` over indexed fields; `ranked` top-K and `having` range queries over ranked indexes |
| [`sdk.contracts`](src/contracts/facade.ts) | Fetch, publish, and update data contracts |
| [`sdk.tokens`](src/tokens/facade.ts) | Mint, burn, transfer, freeze tokens and query balances |
| [`sdk.dpns`](src/dpns/facade.ts) | Register and resolve Dash Platform names |
Expand All @@ -96,6 +100,63 @@ The SDK organises its API into domain-specific facades, each accessible as a pro

A `wallet` namespace is also exported with utilities for BIP39 mnemonic generation and validation, BIP44/DIP9/DIP13 key derivation (path helpers included), extended-key conversion (`xprvToXpub`, `deriveChildPublicKey`), key-pair generation and import (`generateKeyPair`, `keyPairFromWif`, `keyPairFromHex`), public-key-to-address conversion, address validation, message signing, and Dashpay contact-key derivation. See [`src/wallet/functions.ts`](src/wallet/functions.ts) for the full list.

## Ranked queries

From protocol version 14, a contract index can declare `rankedCountable`, `rankedSummable` or `rankedAverageable`. Against such an index the SDK can answer "which groups score highest?" with a proof, in `O(log n + k)`, without walking every group:

```ts
// The three best restaurants by average grade.
const page = await sdk.documents.ranked({
dataContractId: RESTAURANTS,
documentTypeName: 'review',
groupBy: 'restaurantId',
aggregate: { type: 'avg', property: 'grade' },
limit: 3,
});

for (const entry of page.entries) {
// `value` is exact fixed point for the avg axis — divide by `page.valueScale`,
// never by a hardcoded constant. `valueAsNumber` is a lossy display helper.
console.log(entry.rank, entry.groupValue, Number(entry.value) / Number(page.valueScale));
}
```

`limit` is required and capped at `await EvoSDK.maxRankedLimit()` (a hard reject, not a clamp). `offset` skips ranks — `{ limit: 1, offset: 4 }` is "the 5th best" — and has no ceiling, because the skipped region is attested rather than walked.

`sdk.documents.having()` bounds the same axis by value instead of by position (`{ operator: '>', value: 100 }`), and `rankedWithProof` / `havingWithProof` return the proof and block metadata alongside the result.

## Document references (`refersTo`)

Also from protocol version 14, an identifier property can declare what it points at. This is a write-time consensus constraint — nothing resolves a reference for a reader — but a fetched contract can be asked what it declares:

```ts
const contract = await sdk.contracts.fetch(contractId);

for (const ref of contract.documentTypeReferences('note')) {
// { path: 'author', type: 'identityPublicKey', keyIdProperty: 'authorKeyId' }
console.log(ref.path, ref.type);
}

// Every document type that declares at least one reference.
contract.documentReferences;
```

Declarations are only parsed from protocol version 14 onward; a contract deserialized against an earlier version reports none even when its raw schema carries the keyword.

When a write is rejected because a reference does not resolve, the consensus code reaches JS as `error.code`:

```ts
import { DocumentReferenceErrorCode } from '@dashevo/evo-sdk';

try {
await sdk.documents.create({ document, identityKey, signer });
} catch (e) {
if (e.code === DocumentReferenceErrorCode.ReferencedIdentityKeyDisabled) {
// the referenced key exists but was disabled
}
}
```

## Contributing

Feel free to dive in! [Open an issue](https://github.com/dashpay/platform/issues/new/choose) or submit PRs.
Expand Down
33 changes: 33 additions & 0 deletions packages/js-evo-sdk/src/documents/facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,37 @@ export class DocumentsFacade {
const w = await this.sdk.getWasmSdkConnected();
return w.getDocumentsAverageWithProofInfo(query, averageProperty);
}

/**
* Rank groups by an aggregate and return the top (or bottom) `limit` of
* them. Requires protocol version 14 and a contract index declaring the
* matching ranked keyword.
*/
async ranked(query: wasm.DocumentsRankedQuery): Promise<wasm.DocumentsRankedResult> {
const w = await this.sdk.getWasmSdkConnected();
return w.getDocumentsRanked(query);
}

async rankedWithProof(
query: wasm.DocumentsRankedQuery,
): Promise<wasm.ProofMetadataResponseTyped<wasm.DocumentsRankedResult>> {
const w = await this.sdk.getWasmSdkConnected();
return w.getDocumentsRankedWithProofInfo(query);
}

/**
* Return the groups whose aggregate falls inside a bound. Same ranked
* indexes as {@link ranked}, bounded by value rather than by position.
*/
async having(query: wasm.DocumentsHavingQuery): Promise<wasm.DocumentsHavingResult> {
const w = await this.sdk.getWasmSdkConnected();
return w.getDocumentsHaving(query);
}

async havingWithProof(
query: wasm.DocumentsHavingQuery,
): Promise<wasm.ProofMetadataResponseTyped<wasm.DocumentsHavingResult>> {
const w = await this.sdk.getWasmSdkConnected();
return w.getDocumentsHavingWithProofInfo(query);
}
}
20 changes: 20 additions & 0 deletions packages/js-evo-sdk/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,26 @@ export class EvoSDK {
return wasm.WasmSdkBuilder.getLatestVersionNumber();
}

/**
* Hard ceiling on a ranked / having-range `limit`. A request above it is
* rejected, not truncated.
*/
static async maxRankedLimit(): Promise<number> {
await initWasm();
return wasm.WasmSdk.maxRankedLimit();
}

/**
* Fixed-point divisor for the `avg` axis of a ranked / having-range
* result. Exposed so a caller who persisted a `DocumentsGroupEntry.value`
* can re-render it without holding on to the result that produced it.
* Never hardcode the number.
*/
static async rankedAverageScale(): Promise<bigint> {
await initWasm();
return wasm.WasmSdk.rankedAverageScale();
}

// Factory helpers that return configured instances (not connected)
static testnet(options: ConnectionOptions = {}): EvoSDK { return new EvoSDK({ network: 'testnet', ...options }); }
static mainnet(options: ConnectionOptions = {}): EvoSDK { return new EvoSDK({ network: 'mainnet', ...options }); }
Expand Down
146 changes: 146 additions & 0 deletions packages/js-evo-sdk/tests/unit/facades/documents.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,24 @@ describe('DocumentsFacade', () => {
let getDocumentsSumWithProofInfoStub: SinonStub;
let getDocumentsAverageStub: SinonStub;
let getDocumentsAverageWithProofInfoStub: SinonStub;
let getDocumentsRankedStub: SinonStub;
let getDocumentsRankedWithProofInfoStub: SinonStub;
let getDocumentsHavingStub: SinonStub;
let getDocumentsHavingWithProofInfoStub: SinonStub;

const emptyRankedResult = {
startingRank: BigInt(0),
entries: [],
aggregate: 'avg',
groupBy: 'restaurantId',
valueScale: BigInt(1),
};
const emptyHavingResult = {
entries: [],
aggregate: 'count',
groupBy: 'hashtag',
valueScale: BigInt(1),
};

beforeEach(async function setup() {
await init();
Expand Down Expand Up @@ -97,6 +115,20 @@ describe('DocumentsFacade', () => {
proof: {},
metadata: {},
});

// Stub ranked / having-range query methods
getDocumentsRankedStub = this.sinon.stub(wasmSdk, 'getDocumentsRanked').resolves(emptyRankedResult);
getDocumentsRankedWithProofInfoStub = this.sinon.stub(wasmSdk, 'getDocumentsRankedWithProofInfo').resolves({
data: emptyRankedResult,
proof: {},
metadata: {},
});
getDocumentsHavingStub = this.sinon.stub(wasmSdk, 'getDocumentsHaving').resolves(emptyHavingResult);
getDocumentsHavingWithProofInfoStub = this.sinon.stub(wasmSdk, 'getDocumentsHavingWithProofInfo').resolves({
data: emptyHavingResult,
proof: {},
metadata: {},
});
});

describe('query()', () => {
Expand Down Expand Up @@ -386,4 +418,118 @@ describe('DocumentsFacade', () => {
expect(getDocumentsAverageWithProofInfoStub).to.be.calledOnceWithExactly(query, averageProperty);
});
});

describe('ranked()', () => {
it('should rank groups by an aggregate', async () => {
const query = {
dataContractId: 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec',
documentTypeName: 'review',
groupBy: 'restaurantId',
aggregate: { type: 'avg', property: 'grade' },
limit: 3,
};

await client.documents.ranked(query);

expect(getDocumentsRankedStub).to.be.calledOnceWithExactly(query);
});

it('should pass through the offset that selects a single rank', async () => {
// "The 5th best": skip the four above it, take one.
const query = {
dataContractId: 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec',
documentTypeName: 'review',
groupBy: 'restaurantId',
aggregate: { type: 'avg', property: 'grade' },
limit: 1,
offset: 4,
};

await client.documents.ranked(query);

expect(getDocumentsRankedStub).to.be.calledOnceWithExactly(query);
});

it('should pass through the equality pins of a compound ranked index', async () => {
const query = {
dataContractId: 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec',
documentTypeName: 'grade',
groupBy: 'class',
aggregate: { type: 'count' },
where: [['country', '==', 'DE']],
direction: 'asc',
limit: 10,
};

await client.documents.ranked(query);

expect(getDocumentsRankedStub).to.be.calledOnceWithExactly(query);
});
});

describe('rankedWithProof()', () => {
it('should rank groups with proof metadata', async () => {
const query = {
dataContractId: 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec',
documentTypeName: 'review',
groupBy: 'restaurantId',
aggregate: { type: 'count' },
limit: 5,
};

await client.documents.rankedWithProof(query);

expect(getDocumentsRankedWithProofInfoStub).to.be.calledOnceWithExactly(query);
});
});

describe('having()', () => {
it('should bound groups by their aggregate', async () => {
const query = {
dataContractId: 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec',
documentTypeName: 'post',
groupBy: 'hashtag',
aggregate: { type: 'count' },
having: { operator: '>', value: 100 },
direction: 'desc',
limit: 100,
};

await client.documents.having(query);

expect(getDocumentsHavingStub).to.be.calledOnceWithExactly(query);
});

it('should pass through a two-operand between bound', async () => {
const query = {
dataContractId: 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec',
documentTypeName: 'tip',
groupBy: 'recipientId',
aggregate: { type: 'sum', property: 'amount' },
having: { operator: 'between', value: [1000, 5000] },
limit: 25,
};

await client.documents.having(query);

expect(getDocumentsHavingStub).to.be.calledOnceWithExactly(query);
});
});

describe('havingWithProof()', () => {
it('should bound groups with proof metadata', async () => {
const query = {
dataContractId: 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec',
documentTypeName: 'post',
groupBy: 'hashtag',
aggregate: { type: 'count' },
having: { operator: '>=', value: BigInt(1) },
limit: 10,
};

await client.documents.havingWithProof(query);

expect(getDocumentsHavingWithProofInfoStub).to.be.calledOnceWithExactly(query);
});
});
});
20 changes: 20 additions & 0 deletions packages/js-evo-sdk/tests/unit/sdk.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,4 +301,24 @@ describe('EvoSDK', () => {
expect(sdk.options.devnetName).to.be.undefined();
});
});

describe('ranked query constants', () => {
// Documented in the README as the way to discover the ceiling, so the
// forwarding statics have to actually exist on EvoSDK.
it('should expose the ranked limit ceiling', async () => {
const limit = await EvoSDK.maxRankedLimit();

expect(limit).to.be.a('number');
expect(limit).to.be.greaterThan(0);
});

it('should expose the avg fixed-point scale as a bigint', async () => {
// Returned rather than documented as a literal precisely so callers
// never hardcode it — it has already changed once.
const scale = await EvoSDK.rankedAverageScale();

expect(typeof scale).to.equal('bigint');
expect(scale > BigInt(0)).to.equal(true);
});
});
});
Loading
Loading