Skip to content

Instantly share code, notes, and snippets.

@siman
Created October 7, 2019 12:39
Show Gist options
  • Save siman/ee357f79dddf6812a3e44085e387a75a to your computer and use it in GitHub Desktop.
Save siman/ee357f79dddf6812a3e44085e387a75a to your computer and use it in GitHub Desktop.
import { Substrate } from "../cli/substrate";
import { arrayToOneLineString, prettyClass, prettyEntity } from '../cli/printers';
import { AddClassSchemaInputType } from '../types/AddClassSchemaTypes';
const classId = 1
const entityId = 1
const schemaId = 0
// tslint:disable-next-line:max-func-body-length
async function main() {
const sub = new Substrate();
await sub.connect();
sub.setKeypair({
uri: '//Alice',
type: 'sr25519'
})
// Create class
// ------------------------------------------
const newClass = {
name: 'Podcast',
description: 'Desc of podcast class'
}
const newClassRes = await sub.txCreateClass(newClass)
console.log({ newClassRes })
// Get all class ids
// ------------------------------------------
const allClassIds = await sub.getAllClassIds()
.catch(err => console.log('Failed to get list of class ids from Substrate.', err));
console.log(`All class ids:`)
console.log(arrayToOneLineString(allClassIds))
// Add new schema to class
// ------------------------------------------
const newClassSchema: AddClassSchemaInputType = {
classId,
newProperties: [
// {
// required: true,
// type: 'Uint16',
// name: 'episode',
// description: 'A episode nubmer in this podcast'
// },
{
required: true,
type: 'Text',
name: 'author',
description: 'Author of this podcast episode',
maxTextLength: 51
},
{
required: false,
type: 'TextVec',
name: 'guests',
description: 'Guests in this podcast episode',
maxItems: 11,
maxTextLength: 61
}
]
};
await sub.txAddClassSchema(newClassSchema)
// Create new entity
// ------------------------------------------
const newEntity = { classId }
await sub.txCreateEntity(newEntity)
// Add schema support to entity
// ------------------------------------------
const schema_with_values = {
entityId,
schemaId,
propertyValues: [
// {
// name: 'episode',
// value: 123
// },
{
name: 'author',
value: 'Alice'
},
{
name: 'guests',
value: [
'Bob',
'Charlie'
]
}
]
}
await sub.txAddSchemaSupportToEntity(schema_with_values)
// Get Class
// ------------------------------------------
const clazz = await sub.getClassById(classId)
.catch(err => console.log(`Failed to get class by id '${classId}'`, err));
console.log(`Class by id`, classId, prettyClass(clazz))
// Get all entity ids
// ------------------------------------------
const allEntityIds = await sub.getAllEntityIds()
.catch(err => console.log('Failed to get list of all entity ids from Substrate.', err));
console.log(`All entity ids:`)
console.log(arrayToOneLineString(allEntityIds))
// Get Entity
// ------------------------------------------
const entity = await sub.getEntityById(classId)
.catch(err => console.log(`Failed to get entity by id '${entityId}'`, err));
const entityAsText = await prettyEntity(entity, sub)
console.log(`Entity by id`, entityId, entityAsText)
sub.disconnect();
}
main()
// tslint:disable-next-line:import-name
import BN from 'bn.js';
import { ApiPromise, WsProvider } from '@polkadot/api';
import { Keyring } from '@polkadot/keyring';
import { KeyringPair } from '@polkadot/keyring/types';
import { KeypairType } from '@polkadot/util-crypto/types';
import { CodecResult, SubscriptionResult } from '@polkadot/api/promise/types';
import { SubmittableExtrinsic } from '@polkadot/api/SubmittableExtrinsic';
import { RpcEndpoints, greenItem } from './utils';
import { registerJoystreamTypes } from '@joystream/types';
import ClassId from '@joystream/types/lib/versioned-store/ClassId';
import EntityId from '@joystream/types/lib/versioned-store/EntityId';
import { Class, Entity } from '@joystream/types/lib/versioned-store';
import PropertyTypeName from '@joystream/types/lib/versioned-store/PropertyTypeName';
import {
PropertyByNameMap, CreateClassInputType, AddClassSchemaInputType, CreateEntityInputType, AddSchemaSupportToEntityInputType, UpdateEntityPropertyValuesInputType
} from '../types';
import {
transformCreateClass, transformAddClassSchema, transformCreateEntity,
transformAddSchemaSupportToEntity, transformUpdateEntityPropertyValues
} from '../transform';
import { EventData } from '@polkadot/types/type/Event';
export type KeypairProps = {
// Example: '//Alice'
uri: string,
// Example: 'sr25519'
type: KeypairType,
pass?: string
}
export class Substrate {
protected api: ApiPromise
protected keypair: KeyringPair
constructor () {}
public connect = async () => {
const rpcEndpoint = RpcEndpoints.localhost;
const provider = new WsProvider(rpcEndpoint);
// Register types before creating the API:
registerJoystreamTypes();
// Create the API and wait until ready:
console.log(`Connecting to Substrate API: ${rpcEndpoint}`)
this.api = await ApiPromise.create(provider);
// Retrieve the chain & node information information via rpc calls
const system = this.api.rpc.system;
const [ chain, nodeName, nodeVersion ] = await Promise.all(
[ system.chain(), system.name(), system.version() ]);
console.log(`Connected to chain '${chain}' (${nodeName} v${nodeVersion})`)
}
public disconnect = () => {
const { api } = this;
if (api && api.isReady) {
api.disconnect();
console.log(`Disconnect from Substrate API.`);
}
}
/**
* Shortcut for versioned store query.
*/
private vsQuery = () => {
return this.api.query.versionedStore;
}
private vsTx = () => {
return this.api.tx.versionedStore;
}
public setKeypair = (props: KeypairProps) => {
const keyring = new Keyring()
// TODO allow to import account via JSON file:
// const keypair = keyring.addFromJson(accountJson)
// TODO allow to import account via seed.
// const keypair = keyring.addFromSeed(props.seed, null, props.type)
const keypair = keyring.addFromUri(props.uri, null, props.type)
if (keypair.isLocked()) {
if (props.pass) {
keypair.decodePkcs8(props.pass);
} else {
// TODO (improvement) ask a password to this account in terminal
throw new Error(`Not implemented: Get an account pass from terminal`)
}
}
console.log(`Next account will be used for signing txs:`,
{
address: keypair.address(),
type: keypair.type
}
)
this.keypair = keypair;
}
public accountAddress = () => {
return this.keypair.address()
}
public accountBalance = async () => {
return await this.api.query.balances.freeBalance(this.accountAddress()) as unknown as BN
}
public nextClassId = async () => {
return await this.vsQuery().nextClassId() as unknown as ClassId
}
public nextEntityId = async () => {
return await this.vsQuery().nextEntityId() as unknown as EntityId
}
public getAllClassIds = async (): Promise<ClassId[]> => {
const nextId = await this.vsQuery().nextClassId() as unknown as ClassId;
if (!nextId || nextId.lte(new BN(1))) {
return [];
}
const nextIdAsNum = nextId.toNumber();
const ids: ClassId[] = [];
for (let i = 1; i < nextIdAsNum; i++) {
ids.push(new ClassId(i));
}
return ids;
}
public getAllEntityIds = async (): Promise<EntityId[]> => {
const nextId = await this.vsQuery().nextEntityId() as unknown as EntityId;
if (!nextId || nextId.lte(new BN(1))) {
return [];
}
const nextIdAsNum = nextId.toNumber();
const ids: EntityId[] = [];
for (let i = 1; i < nextIdAsNum; i++) {
ids.push(new EntityId(i));
}
return ids;
}
public getClassById = async (id: ClassId | number): Promise<Class> => {
return await this.vsQuery().classById(id) as unknown as Class
}
public getEntityById = async (id: EntityId | number): Promise<Entity> => {
return await this.vsQuery().entityById(id) as unknown as Entity
}
private signTxAndSend = async (tx: SubmittableExtrinsic<CodecResult, SubscriptionResult>, interestingEventName?: string): Promise<EventData | undefined> => {
const balance = await this.accountBalance()
console.log(`Account balance:`, balance.toString(), 'tokens')
if (balance.lt(new BN(1))) {
console.log(`Not enough tokens to execute a tx`)
return undefined
}
// Get the nonce for this account:
const nonce = await this.api.query.system.accountNonce(this.keypair.address()) as unknown as Uint8Array;
return await new Promise<EventData>((resolve, reject) => tx
.sign(this.keypair, { nonce })
.send(({ events = [], status }) => {
console.log('Transaction status:', status.type);
if (status.isFinalized) {
console.log('Completed at block hash', status.asFinalized.toHex());
console.log('Events:');
let eventData: EventData = undefined
events.forEach(({ phase, event: { data, method, section } }) => {
console.log('\t', phase.toString(), `: ${section}.${method}`, data.toString());
if (interestingEventName && interestingEventName === method) {
eventData = data
}
});
// process.exit(0);
resolve(eventData)
}
}).catch(reject)
)
}
public getClassPropertyMap = async (classId: ClassId): Promise<PropertyByNameMap> => {
const clazz = await this.getClassById(classId)
if (!clazz || clazz.id.isZero()) {
throw new Error(`Class not found by id ${classId.toNumber()}`)
}
const map: PropertyByNameMap = new Map();
clazz.properties.map((prop, index) => {
const type = prop.prop_type.type as PropertyTypeName
map.set(prop.name.toString(), { index, type })
})
return map
}
public txCreateClass = async (input: CreateClassInputType) => {
const txName = 'testCreateClass'
const { error, result } = transformCreateClass(input)
if (error) {
console.log(`Cannot parse input data for tx '${txName}'`, error)
return undefined
}
const res = await this.signTxAndSend(
this.vsTx()[txName](
result.name,
result.description
),
'ClassCreated'
)
console.log(`Tx executed:`, greenItem(txName))
return res
}
public txAddClassSchema = async (input: AddClassSchemaInputType) => {
const txName = 'testAddClassSchema'
const classId = new ClassId(input.classId)
const propMap = await this.getClassPropertyMap(classId)
const { error, result } = transformAddClassSchema(input, propMap)
if (error) {
console.log(`Cannot parse input data for tx '${txName}'`, error)
return undefined
}
const res = await this.signTxAndSend(
this.vsTx()[txName](
result.class_id,
result.existing_properties,
result.new_properties
),
'ClassSchemaAdded'
)
console.log(`Tx executed:`, greenItem(txName))
return res
}
public txCreateEntity = async (input: CreateEntityInputType) => {
const txName = 'testCreateEntity'
const { error, result } = transformCreateEntity(input)
if (error) {
console.log(`Cannot parse input data for tx '${txName}'`, error)
return undefined
}
const res = await this.signTxAndSend(
this.vsTx()[txName](
result.class_id
),
'EntityCreated'
)
console.log(`Tx executed:`, greenItem(txName))
return res
}
public txAddSchemaSupportToEntity = async (input: AddSchemaSupportToEntityInputType) => {
const txName = 'testAddSchemaSupportToEntity'
const entityId = new EntityId(input.entityId)
const entity = await this.getEntityById(entityId)
const propMap = await this.getClassPropertyMap(entity.class_id)
const { error, result } = transformAddSchemaSupportToEntity(input, propMap)
if (error) {
console.log(`Cannot parse input data for tx '${txName}'`, error)
return undefined
}
const res = await this.signTxAndSend(
this.vsTx()[txName](
result.entity_id,
result.schema_id,
result.property_values
),
'EntitySchemaAdded'
)
console.log(`Tx executed:`, greenItem(txName))
return res
}
public txUpdateEntityPropertyValues = async (input: UpdateEntityPropertyValuesInputType) => {
const txName = 'testUpdateEntityPropertyValues'
const entityId = new EntityId(input.entityId)
const entity = await this.getEntityById(entityId)
const propMap = await this.getClassPropertyMap(entity.class_id)
const { error, result } = transformUpdateEntityPropertyValues(input, propMap)
if (error) {
console.log(`Cannot parse input data for tx '${txName}'`, error)
return undefined
}
const res = await this.signTxAndSend(
this.vsTx()[txName](
result.entity_id,
result.new_property_values
),
'EntityPropertiesUpdated'
)
console.log(`Tx executed:`, greenItem(txName))
return res
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment