Home Reference Source

packages/oo7-substrate/src/bonds.js

const { camel } = require('change-case');
const { Bond, TransformBond, TimeBond } = require('oo7')
const { nodeService } = require('./nodeService')
const { SubscriptionBond } = require('./subscriptionBond')
const { BlockNumber, Hash } = require('./types');
const { decode, encode } = require('./codec');
const { stringToBytes, hexToBytes, bytesToHex, toLE } = require('./utils')
const { StorageBond } = require('./storageBond')
const { setMetadata } = require('./metadata')

let chain = (() => {
	let head = new SubscriptionBond('chain_newHead').subscriptable()
	let finalisedHead = new SubscriptionBond('chain_finalisedHead').subscriptable()
	let height = head.map(h => new BlockNumber(h.number))
	let finalisedHeight = finalisedHead.map(h => new BlockNumber(h.number))
	let lag = Bond.all([height, finalisedHeight]).map(([h, f]) => new BlockNumber(h - f))
	let header = hashBond => new TransformBond(hash => nodeService().request('chain_getHeader', [hash]), [hashBond]).subscriptable()
	let block = hashBond => new TransformBond(hash => nodeService().request('chain_getBlock', [hash]), [hashBond]).subscriptable()
	let hash = numberBond => new TransformBond(number => nodeService().request('chain_getBlockHash', [number]), [numberBond])
	return { head, finalisedHead, height, finalisedHeight, header, hash, block, lag }
})()

let system = (() => {
	let time = new TimeBond
	let name = new TransformBond(() => nodeService().request('system_name')).subscriptable()
	let version = new TransformBond(() => nodeService().request('system_version')).subscriptable()
	let chain = new TransformBond(() => nodeService().request('system_chain')).subscriptable()
	let properties = new TransformBond(() => nodeService().request('system_properties')).subscriptable()
	let health = new TransformBond(() => nodeService().request('system_health'), [], [time]).subscriptable()
	let peers = new TransformBond(() => nodeService().request('system_peers'), [], [time]).subscriptable()
	let pendingTransactions = new TransformBond(() => nodeService().request('author_pendingExtrinsics')).subscriptable()
	return { name, version, chain, properties, pendingTransactions, health, peers }
})()

let version = (new SubscriptionBond('state_runtimeVersion', [], r => {
	let apis = {}
	r.apis.forEach(([id, version]) => {
		if (typeof id !== 'string') {
			id = String.fromCharCode.apply(null, id)
		}
		apis[id] = version
	})
	return {
		authoringVersion: r.authoringVersion,
		implName: r.implName,
		implVersion: r.implVersion,
		specName: r.specName,
		specVersion: r.specVersion,
		apis
	}
})).subscriptable()

let runtime = {
	version, 
	metadata: new Bond,
	core: (() => {
		let authorityCount = new SubscriptionBond('state_storage', [['0x' + bytesToHex(stringToBytes(':auth:len'))]], r => decode(hexToBytes(r.changes[0][1]), 'u32'))
		let authorities = authorityCount.map(
			n => [...Array(n)].map((_, i) =>
				new SubscriptionBond('state_storage',
					[[ '0x' + bytesToHex(stringToBytes(":auth:")) + bytesToHex(toLE(i, 4)) ]],
					r => decode(hexToBytes(r.changes[0][1]), 'AccountId')
				)
			), 2)
		let code = new SubscriptionBond('state_storage', [['0x' + bytesToHex(stringToBytes(':code'))]], r => hexToBytes(r.changes[0][1]))
		let codeHash = new TransformBond(() => nodeService().request('state_getStorageHash', ['0x' + bytesToHex(stringToBytes(":code"))]).then(hexToBytes), [], [version])
		let codeSize = new TransformBond(() => nodeService().request('state_getStorageSize', ['0x' + bytesToHex(stringToBytes(":code"))]), [], [version])
		let heapPages = new SubscriptionBond('state_storage', [['0x' + bytesToHex(stringToBytes(':heappages'))]], r => decode(hexToBytes(r.changes[0][1]), 'u64'))
		return { authorityCount, authorities, code, codeHash, codeSize, version, heapPages }
	})()
}

let calls = {}

class RuntimeUp extends Bond {
	initialise() {
		let that = this
		initRuntime(() => that.trigger(true))
	}
}
let runtimeUp = new RuntimeUp

let onRuntimeInit = []

function initialiseFromMetadata (md) {
	console.log("initialiseFromMetadata", md)
	setMetadata(md)
	let callIndex = 0;
	md.modules.forEach((m) => {
		let o = {}
		let c = {}
		if (m.storage) {
			let storePrefix = m.prefix
			m.storage.forEach(item => {
				switch (item.type.option) {
					case 'Plain': {
						o[camel(item.name)] = new StorageBond(`${storePrefix} ${item.name}`, item.type.value, [], item.default)
						break
					}
					case 'Map': {
						let keyType = item.type.value.key
						let valueType = item.type.value.value
						o[camel(item.name)] = keyBond => new TransformBond(
							key => new StorageBond(`${storePrefix} ${item.name}`, valueType, encode(key, keyType), item.default),
							[keyBond]
						).subscriptable()
						break
					}
				}
			})
		}
		if (m.calls) {
			let thisCallIndex = callIndex
			callIndex++
			m.calls.forEach((item, id) => {
				if (item.arguments.length > 0 && item.arguments[0].name == 'origin' && item.arguments[0].type == 'Origin') {
					item.arguments = item.arguments.slice(1)
				}
				c[camel(item.name)] = function (...bondArgs) {
					if (bondArgs.length != item.arguments.length) {
						throw `Invalid number of argments (${bondArgs.length} given, ${item.arguments.length} expected)`
					}
					return new TransformBond(args => {
						let encoded_args = encode(args, item.arguments.map(x => x.type))
						let res = new Uint8Array([thisCallIndex, id, ...encoded_args]);
						console.log(`Encoding call ${m.name}.${item.name} (${thisCallIndex}.${id}): ${bytesToHex(res)}`)
						return res
					}, [bondArgs], [], 3, 3, undefined, true)
				}
				c[camel(item.name)].help = item.arguments.map(a => a.name)
			})				
		}
		runtime[camel(m.name)] = o
		calls[camel(m.name)] = c
	})
	md.modules.forEach(m => {
		if (m.storage) {
			try {
				require(`./srml/${m.name}`).augment(runtime, chain)
			}
			catch (e) {
				if (!e.toString().startsWith('Error: Cannot find module')) {
					throw e
				}
			}
		}
	})
	if (onRuntimeInit !== null) {
		onRuntimeInit.forEach(f => { if (f) f() })
		onRuntimeInit = null
	}

	runtime.metadata.trigger(md)
}

function decodeMetadata(bytes) {
	let input = { data: bytes }
	let head = decode(input, 'MetadataHead')
	if (head.magic === 0x6174656d) {
		if (head.version == 1) {
			return decode(input, 'MetadataBody')
		} else {
			throw `Metadata version ${head.version} not supported`
		}
	} else {
		let md = decode(bytes, 'Legacy_RuntimeMetadata')
		md.modules = md.modules.map(m => {
			m.name = m.prefix
			m.prefix = m.storage ? m.storage.prefix : null
			m.storage = m.storage ? m.storage.items : null
			m.calls = m.module && m.module.call ? m.module.call.functions : null
			return m
		})
		return md
	}
}

function initRuntime (callback = null) {
	if (onRuntimeInit instanceof Array) {
		onRuntimeInit.push(callback)
		version.tie(() => {
//			console.info("Initialising runtime")
			nodeService().request('state_getMetadata')
				.then(blob => decodeMetadata(hexToBytes(blob)))
				.then(initialiseFromMetadata)
		})
	} else {
		// already inited runtime
		if (callback) {
			callback()
		}
	}
}

function runtimePromise() {
	return new Promise((resolve, reject) => initRuntime(() => resolve(runtime)))
}

function callsPromise() {
	return new Promise((resolve, reject) => initRuntime(() => resolve(calls)))
}

module.exports = { initRuntime, runtimeUp, runtimePromise, callsPromise, runtime, calls, chain, system }