Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Frequency schema names #46

Merged
merged 6 commits into from
Jan 9, 2024
Merged
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
36 changes: 34 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,43 @@ npm install @dsnp/frequency-schemas
### Use Schema

```typescript
import { dsnp } from "frequency-schemas";
import { dsnp } from "@dsnp/frequency-schemas";

console.log(dsnp.getSchema("broadcast"));
```

### Get Schema Id from Chain

```typescript
import { dsnp } from "@dsnp/frequency-schemas";
import { ApiPromise } from "@polkadot/api";

const api = ApiPromise.create(/* ... */);
console.log(await dsnp.getSchemaId(api, "broadcast"));
```

Frequency mainnet and testnet have well-known Ids defined in `dsnp/index.ts`.
Other configurations default to assuming `npm run deploy` has been run on a fresh chain (which is usually the case for a localhost instance), but can be overridden:

```
dsnp.setSchemaMapping(api.genesisHash.toString(), {
// format is dsnpName: { version: schemaId, ... }
"tombstone": { "1.2": 64 },
"broadcast": { "1.2": 67 },
// ...
});

console.log(await dsnp.getSchemaId(api, "broadcast")); // yields 67
```

### With Parquet Writer

```sh
npm install @dsnp/parquetjs
```

```typescript
import { parquet } from "frequency-schemas";
import { parquet } from "@dsnp/frequency-schemas";
import { ParquetWriter } from "@dsnp/parquetjs";

const [parquetSchema, writerOptions] = parquet.fromFrequencySchema("broadcast");
Expand Down Expand Up @@ -150,6 +174,14 @@ There are 8 schemas on the connected chain.
...
```

## Find Frequency Schema Ids that Match DSNP Schema Versions

This script will look up and verify schemas in the schema registry that match the DSNP names and versions defined in `dsnp/index.ts`.

```sh
DEPLOY_SCHEMA_ENDPOINT_URL="ws://127.0.0.1:9944" npm run find
```

## Use with Docker

This repo deploys `dsnp/instant-seal-node-with-deployed-schemas` to Docker Hub.
Expand Down
50 changes: 31 additions & 19 deletions cli/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getFrequencyAPI, getSignerAccountKeys } from "./services/connect.js";
import dsnp, { SchemaName as DsnpSchemaName } from "../dsnp/index.js";
import dsnp, { SchemaName as DsnpSchemaName, SchemaMapping, GENESIS_HASH_MAINNET } from "../dsnp/index.js";
import { EventRecord } from "@polkadot/types/interfaces";

export const deploy = async () => {
Expand Down Expand Up @@ -42,7 +42,8 @@ export const deploy = async () => {

console.log("Deploy of Schemas Starting...");

return await createSchemas(schemaNames);
const mapping = await createSchemas(schemaNames);
console.log("Generated schema mapping:\n", JSON.stringify(mapping, null, 2));
};

// Given a list of events, a section and a method,
Expand All @@ -54,17 +55,13 @@ const eventWithSectionAndMethod = (events: EventRecord[], section: string, metho

// Given a list of schema names, attempt to create them with the chain.
const createSchemas = async (schemaNames: string[]) => {
type SchemaInfo = {
schemaName: string;
id?: number;
};
type SchemaInfo = [schemaName: DsnpSchemaName, { [version: string]: number }];

const promises: Promise<SchemaInfo>[] = [];
const api = await getFrequencyAPI();
const signerAccountKeys = getSignerAccountKeys();
// Mainnet genesis hash means we should propose instead of create
const shouldPropose =
api.genesisHash.toHex() === "0x4a587bf17a404e3572747add7aab7bbe56e805a5479c6c436f07f36fcc8d3ae1";
const shouldPropose = api.genesisHash.toHex() === GENESIS_HASH_MAINNET;

if (shouldPropose && schemaNames.length > 1) {
console.error("Proposing to create schemas can only occur one at a time. Please try again with only one schema.");
Expand Down Expand Up @@ -92,55 +89,70 @@ const createSchemas = async (schemaNames: string[]) => {
// Propose to create
const promise = new Promise<SchemaInfo>((resolve, reject) => {
api.tx.schemas
.proposeToCreateSchema(
.proposeToCreateSchemaV2(
json_no_ws,
schemaDeploy.modelType,
schemaDeploy.payloadLocation,
schemaDeploy.settings,
"dsnp." + schemaName,
)
.signAndSend(signerAccountKeys, { nonce }, ({ status, events, dispatchError }) => {
if (dispatchError) {
console.error("ERROR: ", dispatchError.toHuman());
console.log("Might already have a proposal with the same hash?");
reject();
reject(dispatchError.toHuman());
} else if (status.isInBlock || status.isFinalized) {
const evt = eventWithSectionAndMethod(events, "council", "Proposed");
if (evt) {
const id = evt?.data[1];
const hash = evt?.data[2].toHex();
console.log("SUCCESS: " + schemaName + " schema proposed with id of " + id + " and hash of " + hash);
resolve({ schemaName, id: Number(id.toHuman()) });
} else resolve({ schemaName });
const v2n = Object.fromEntries([[schemaDeploy.dsnpVersion, Number(id.toHuman())]]);
resolve([schemaName as DsnpSchemaName, v2n]);
} else {
const err = "Proposed event not found";
console.error(`ERROR: ${err}`);
reject(err);
}
}
});
});
promises[idx] = promise;
} else {
// Create directly via sudo
const tx = api.tx.schemas.createSchemaViaGovernance(
const tx = api.tx.schemas.createSchemaViaGovernanceV2(
signerAccountKeys.address,
json_no_ws,
schemaDeploy.modelType,
schemaDeploy.payloadLocation,
schemaDeploy.settings,
"dsnp." + schemaName,
);
const promise = new Promise<SchemaInfo>((resolve, reject) => {
api.tx.sudo.sudo(tx).signAndSend(signerAccountKeys, { nonce }, ({ status, events, dispatchError }) => {
if (dispatchError) {
console.error("ERROR: ", dispatchError.toHuman());
reject();
reject(dispatchError.toHuman());
} else if (status.isInBlock || status.isFinalized) {
const evt = eventWithSectionAndMethod(events, "schemas", "SchemaCreated");
if (evt) {
const val = evt?.data[1];
console.log("SUCCESS: " + schemaName + " schema created with id of " + val);
resolve({ schemaName, id: Number(val.toHuman()) });
} else resolve({ schemaName });
const id = evt?.data[1];
Copy link
Collaborator

@enddynayn enddynayn Jan 4, 2024

Choose a reason for hiding this comment

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

maybe not a big deal for your purposes but do you need to close the subscription?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think so, for correctness. Probably doesn't matter too much since it's a run-and-done CLI program, but I wouldn't be surprised if there are some error logs that come out when the program is done that would be cleaned up with an unsub() call. I'll pull this branch and see...

Copy link
Collaborator

Choose a reason for hiding this comment

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

I see no errors when I run; like I said, probably not a critical issue since this is CLI run-and-done. Fix if you like; non-blocking.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think we're actually subscribed here -- if the event is not immediately in the block or finalized, it looks like we never resolve or reject the Promise. Obviously this seems to work fine in an ideal environment (especially one with instant sealing) but I don't think it handles edge cases or busy chains very well.

I'd prefer to make that a separate issue though as it's a pre-existing condition, and maybe someone slightly more familiar with how to subscribe and poll could add this or give me some pointers.

Copy link
Member Author

Choose a reason for hiding this comment

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

Added as #49

console.log("SUCCESS: " + schemaName + " schema created with id of " + id);
const v2n = Object.fromEntries([[schemaDeploy.dsnpVersion, Number(id.toHuman())]]);
resolve([schemaName as DsnpSchemaName, v2n]);
} else {
const err = "SchemaCreated event not found";
console.error(`ERROR: ${err}`);
reject(err);
}
}
});
});
promises[idx] = promise;
}
}
return Promise.all(promises);
const output = await Promise.all(promises);
const mapping: { [genesisHash: string]: SchemaMapping } = {};
mapping[api.genesisHash.toString()] = Object.fromEntries(output);
return mapping;
};
44 changes: 44 additions & 0 deletions cli/find.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { getFrequencyAPI } from "./services/connect.js";
import { schemas } from "../dsnp/index.js";

const find = async () => {
const api = await getFrequencyAPI();

console.log("\n## DSNP Schema Information");

const allDsnp = (await api.rpc.schemas.getVersions("dsnp")).unwrap();
for (const schemaEntry of schemas.entries()) {
const schemaString = JSON.stringify(schemaEntry[1].model);
const schemaVersionResult = allDsnp.filter(
(versioned) => versioned.schema_name.toString() === "dsnp." + schemaEntry[0],
);
for (const version of schemaVersionResult) {
const schemaResult = (await api.rpc.schemas.getBySchemaId(version.schema_id.toString())).unwrap();
const jsonSchema = Buffer.from(schemaResult.model).toString("utf8");
const { schema_id, model_type, payload_location, settings } = schemaResult;

// Ensure that full entry details match, otherwise it's a different version
if (
schemaString === jsonSchema &&
model_type.toHuman() === schemaEntry[1].modelType &&
payload_location.toHuman() === schemaEntry[1].payloadLocation &&
JSON.stringify(settings.toHuman()) === JSON.stringify(schemaEntry[1].settings)
) {
console.log(`\n## Schema Id ${schema_id}`);
console.table(
Object.entries({
schemaName: schemaEntry[0],
dsnpVersion: schemaEntry[1].dsnpVersion,
schemaId: schema_id.toHuman(),
}).map(([key, value]) => ({ key, value })),
);
}
}
}
};

export const main = async () => {
await find();
};

main().catch(console.error).finally(process.exit);
Loading