packages/oo7-substrate/src/codec.js
const { TextDecoder } = require('text-encoding')
const { ss58Decode } = require('./ss58')
const { VecU8, AccountId, Hash, Signature, VoteThreshold, SlashPreference, Moment, Balance,
BlockNumber, AccountIndex, Tuple, TransactionEra, Perbill, Permill } = require('./types')
const { toLE, leToNumber, leToSigned, bytesToHex, hexToBytes } = require('./utils')
const { metadata } = require('./metadata')
const transforms = {
Legacy_RuntimeMetadata: { outerEvent: 'Legacy_OuterEventMetadata', modules: 'Vec<Legacy_RuntimeModuleMetadata>', outerDispatch: 'Legacy_OuterDispatchMetadata' },
Legacy_OuterDispatchMetadata: { name: 'String', calls: 'Vec<Legacy_OuterDispatchCall>' },
Legacy_OuterDispatchCall: { name: 'String', prefix: 'String', index: 'u16' },
Legacy_RuntimeModuleMetadata: { prefix: 'String', module: 'Legacy_ModuleMetadata', storage: 'Option<Legacy_StorageMetadata>' },
Legacy_StorageFunctionModifier: { _enum: [ 'Optional', 'Default' ] },
Legacy_StorageFunctionTypeMap: { key: 'Type', value: 'Type' },
Legacy_StorageFunctionType: { _enum: { Plain: 'Type', Map: 'Legacy_StorageFunctionTypeMap' } },
Legacy_StorageFunctionMetadata: {
name: 'String',
modifier: 'Legacy_StorageFunctionModifier',
type: 'Legacy_StorageFunctionType',
default: 'Vec<u8>',
documentation: 'Vec<String>',
_post: x => {
try {
if (x.default) {
x.default = decode(
x.default,
x.type.option === 'Plain' ? x.type.value : x.type.value.value
)
}
}
catch (e) {
x.default = null
}
}
},
Legacy_StorageMetadata: { prefix: 'String', items: 'Vec<Legacy_StorageFunctionMetadata>' },
Legacy_EventMetadata: { name: 'String', arguments: 'Vec<Type>', documentation: 'Vec<String>' },
Legacy_OuterEventMetadata: { name: 'String', events: 'Vec<(String, Vec<Legacy_EventMetadata>)>' },
Legacy_ModuleMetadata: { name: 'String', call: 'Legacy_CallMetadata' },
Legacy_CallMetadata: { name: 'String', functions: 'Vec<Legacy_FunctionMetadata>' },
Legacy_FunctionMetadata: { id: 'u16', name: 'String', arguments: 'Vec<Legacy_FunctionArgumentMetadata>', documentation: 'Vec<String>' },
Legacy_FunctionArgumentMetadata: { name: 'String', type: 'Type' },
MetadataHead: { magic: 'u32', version: 'u8' },
MetadataBody: { modules: 'Vec<MetadataModule>' },
MetadataModule: {
name: 'String',
prefix: 'String',
storage: 'Option<Vec<MetadataStorage>>',
calls: 'Option<Vec<MetadataCall>>',
events: 'Option<Vec<MetadataEvent>>',
},
MetadataStorage: {
name: 'String',
modifier: { _enum: [ 'Optional', 'Default' ] },
type: { _enum: { Plain: 'Type', Map: { key: 'Type', value: 'Type' } } },
default: 'Vec<u8>',
documentation: 'Docs',
_post: x => {
try {
if (x.default) {
x.default = decode(
x.default,
x.type.option === 'Plain' ? x.type.value : x.type.value.value
)
}
}
catch (e) {
x.default = null
}
}
},
MetadataCall: {
name: 'String',
arguments: 'Vec<MetadataCallArg>',
documentation: 'Docs',
},
MetadataCallArg: { name: 'String', type: 'Type' },
MetadataEvent: {
name: 'String',
arguments: 'Vec<Type>',
documentation: 'Docs',
},
Docs: 'Vec<String>',
NewAccountOutcome: { _enum: [ 'NoHint', 'GoodHint', 'BadHint' ] },
UpdateBalanceOutcome: { _enum: [ 'Updated', 'AccountKilled' ] },
Transaction: { version: 'u8', sender: 'Address', signature: 'Signature', index: 'Compact<Index>', era: 'TransactionEra', call: 'Call' },
Phase: { _enum: { ApplyExtrinsic: 'u32', Finalization: undefined } },
EventRecord: { phase: 'Phase', event: 'Event' },
"<LookupasStaticLookup>::Source": 'Address',
"RawAddress<AccountId,AccountIndex>": 'Address',
"Address<AccountId,AccountIndex>": 'Address',
ParaId: 'u32',
VoteIndex: 'u32',
PropIndex: 'u32',
ReferendumIndex: 'u32',
Index: 'u64',
KeyValue: '(Vec<u8>, Vec<u8>)',
ParaId: 'u32'
};
function addCodecTransform(type, transform) {
if (!transforms[type]) {
transforms[type] = transform
}
}
var decodePrefix = '';
function decode(input, type) {
if (typeof input.data === 'undefined') {
input = { data: input };
}
if (typeof type === 'object') {
let res = {};
if (type instanceof Array) {
// just a tuple
res = new Tuple(type.map(t => decode(input, t)));
} else if (!type._enum) {
// a struct
Object.keys(type).forEach(k => {
if (k != '_post') {
res[k] = decode(input, type[k])
}
});
} else if (type._enum instanceof Array) {
// simple enum
let n = input.data[0];
input.data = input.data.slice(1);
res = { option: type._enum[n] };
} else if (type._enum) {
// enum
let n = input.data[0];
input.data = input.data.slice(1);
let option = Object.keys(type._enum)[n];
res = { option, value: typeof type._enum[option] === 'undefined' ? undefined : decode(input, type._enum[option]) };
}
if (type._post) {
type._post(res)
}
return res;
}
type = type.replace(/ /g, '').replace(/^(T::)+/, '');
if (type == 'EventRecord<Event>') {
type = 'EventRecord'
}
let reencodeCompact;
let p1 = type.match(/^<([A-Z][A-Za-z0-9]*)asHasCompact>::Type$/);
if (p1) {
reencodeCompact = p1[1]
}
let p2 = type.match(/^Compact<([A-Za-z][A-Za-z0-9]*)>$/);
if (p2) {
reencodeCompact = p2[1]
}
if (reencodeCompact) {
return decode(encode(decode(input, 'Compact'), reencodeCompact), reencodeCompact);
}
let dataHex = bytesToHex(input.data.slice(0, 50));
// console.log(decodePrefix + 'des >>>', type, dataHex);
// decodePrefix += " ";
let res;
let transform = transforms[type];
if (transform) {
res = decode(input, transform);
res._type = type;
} else {
switch (type) {
case 'Call':
case 'Proposal': {
throw "Cannot represent Call/Proposal"
}
case 'Event': {
let events = metadata().outerEvent.events
let moduleIndex = decode(input, 'u8')
let module = events[moduleIndex][0]
let eventIndex = decode(input, 'u8')
let name = events[moduleIndex][1][eventIndex].name
let args = decode(input, events[moduleIndex][1][eventIndex].arguments)
res = { _type: 'Event', module, name, args }
break
}
case 'AccountId': {
res = new AccountId(input.data.slice(0, 32));
input.data = input.data.slice(32);
break;
}
case 'Hash': {
res = new Hash(input.data.slice(0, 32));
input.data = input.data.slice(32);
break;
}
case 'Signature': {
res = new Signature(input.data.slice(0,64));
input.data = input.data.slice(64);
break;
}
case 'Balance': {
res = leToNumber(input.data.slice(0, 16));
input.data = input.data.slice(16);
res = new Balance(res);
break;
}
case 'BlockNumber': {
res = leToNumber(input.data.slice(0, 8));
input.data = input.data.slice(8);
res = new BlockNumber(res);
break;
}
case 'AccountIndex': {
res = leToNumber(input.data.slice(0, 4));
input.data = input.data.slice(4);
res = new AccountIndex(res);
break;
}
case 'Moment': {
let n = leToNumber(input.data.slice(0, 8));
input.data = input.data.slice(8);
res = new Moment(n);
break;
}
case 'VoteThreshold': {
const VOTE_THRESHOLD = ['SuperMajorityApprove', 'NotSuperMajorityAgainst', 'SimpleMajority'];
res = new VoteThreshold(VOTE_THRESHOLD[input.data[0]]);
input.data = input.data.slice(1);
break;
}
case 'SlashPreference': {
res = new SlashPreference(decode(input, 'u32'));
break;
}
case 'Perbill': {
res = new Perbill(decode(input, 'u32') / 1000000000.0);
break;
}
case 'Permill': {
res = new Permill(decode(input, 'u32') / 1000000.0);
break;
}
case 'Compact': {
let len;
if (input.data[0] % 4 == 0) {
// one byte
res = input.data[0] >> 2;
len = 1;
} else if (input.data[0] % 4 == 1) {
res = leToNumber(input.data.slice(0, 2)) >> 2;
len = 2;
} else if (input.data[0] % 4 == 2) {
res = leToNumber(input.data.slice(0, 4)) >> 2;
len = 4;
} else {
let n = (input.data[0] >> 2) + 4;
res = leToNumber(input.data.slice(1, n + 1));
len = 1 + n;
}
input.data = input.data.slice(len);
break;
}
case 'u8':
res = input.data.slice(0, 1);
input.data = input.data.slice(1);
break;
case 'u16':
res = leToNumber(input.data.slice(0, 2));
input.data = input.data.slice(2);
break;
case 'u32': {
res = leToNumber(input.data.slice(0, 4));
input.data = input.data.slice(4);
break;
}
case 'u64': {
res = leToNumber(input.data.slice(0, 8));
input.data = input.data.slice(8);
break;
}
case 'u128': {
res = leToNumber(input.data.slice(0, 16));
input.data = input.data.slice(16);
break;
}
case 'i8': {
res = leToSigned(input.data.slice(0, 1));
input.data = input.data.slice(1);
break;
}
case 'i16':
res = leToSigned(input.data.slice(0, 2));
input.data = input.data.slice(2);
break;
case 'i32': {
res = leToSigned(input.data.slice(0, 4));
input.data = input.data.slice(4);
break;
}
case 'i64': {
res = leToSigned(input.data.slice(0, 8));
input.data = input.data.slice(8);
break;
}
case 'i128': {
res = leToSigned(input.data.slice(0, 16));
input.data = input.data.slice(16);
break;
}
case 'bool': {
res = !!input.data[0];
input.data = input.data.slice(1);
break;
}
case 'Vec<bool>': {
let size = decode(input, 'Compact<u32>');
res = [...input.data.slice(0, size)].map(a => !!a);
input.data = input.data.slice(size);
break;
}
case 'Vec<u8>': {
let size = decode(input, 'Compact<u32>');
res = input.data.slice(0, size);
input.data = input.data.slice(size);
break;
}
case 'String': {
let size = decode(input, 'Compact<u32>');
res = input.data.slice(0, size);
input.data = input.data.slice(size);
res = new TextDecoder("utf-8").decode(res);
break;
}
case 'Type': {
res = decode(input, 'String');
while (res.indexOf('T::') != -1) {
res = res.replace('T::', '');
}
res = res.match(/^Box<.*>$/) ? res.slice(4, -1) : res;
break;
}
default: {
let v = type.match(/^Vec<(.*)>$/);
if (v) {
let size = decode(input, 'Compact<u32>');
res = [...new Array(size)].map(() => decode(input, v[1]));
break;
}
let o = type.match(/^Option<(.*)>$/);
if (o) {
let some = decode(input, 'bool');
if (some) {
res = decode(input, o[1]);
} else {
res = null;
}
break;
}
let t = type.match(/^\((.*)\)$/);
if (t) {
res = new Tuple(...decode(input, t[1].split(',')));
break;
}
throw 'Unknown type to decode: ' + type;
}
}
}
// decodePrefix = decodePrefix.substr(3);
// console.log(decodePrefix + 'des <<<', type, res);
return res;
}
function encode(value, type = null) {
// if an array then just concat
if (type instanceof Array) {
if (value instanceof Array) {
let x = value.map((i, index) => encode(i, type[index]));
let res = new Uint8Array();
x.forEach(x => {
r = new Uint8Array(res.length + x.length);
r.set(res)
r.set(x, res.length)
res = r
})
return res
} else {
throw 'If type is array, value must be too'
}
}
if (typeof value == 'object' && !type && value._type) {
type = value._type
}
if (typeof type != 'string') {
throw 'type must be either an array or a string'
}
type = type.replace(/ /g, '').replace(/^(T::)+/, '');
if (typeof value == 'string' && value.startsWith('0x')) {
value = hexToBytes(value)
}
if (transforms[type]) {
let transform = transforms[type]
if (transform instanceof Array || typeof transform == 'string') {
// just a tuple or string
return encode(value, transform)
} else if (!transform._enum) {
// a struct
let keys = []
let types = []
Object.keys(transform).forEach(k => {
keys.push(value[k])
types.push(transform[k])
})
return encode(keys, types)
} else if (transform._enum instanceof Array) {
// simple enum
return new Uint8Array([transform._enum.indexOf(value.option)])
} else if (transform._enum) {
// enum
let index = Object.keys(transform._enum).indexOf(value.option)
let value = encode(value.value, transform._enum[value.option])
return new Uint8Array([index, ...value])
}
}
// other type-specific transforms
if (type == 'Vec<u8>') {
if (typeof value == 'object' && value instanceof Uint8Array) {
return new Uint8Array([...encode(value.length, 'Compact<u32>'), ...value])
}
}
let match_vec = type.match(/^Vec<(.*)>$/);
if (match_vec) {
if (value instanceof Array) {
let res = new Uint8Array([...encode(value.length, 'Compact<u32>')])
value.forEach(v => {
let x = encode(v, match_vec[1])
r = new Uint8Array(res.length + x.length)
r.set(res)
r.set(x, res.length)
res = r
})
return res
}
}
let t = type.match(/^\((.*)\)$/)
if (t) {
return encode(value, t[1].split(','))
}
if (type == 'Address') {
if (typeof value == 'string') {
value = ss58Decode(value)
}
if (typeof value == 'object' && value instanceof Uint8Array && value.length == 32) {
return new Uint8Array([0xff, ...value])
}
if (typeof value == 'number' || value instanceof AccountIndex) {
if (value < 0xf0) {
return new Uint8Array([value])
} else if (value < 1 << 16) {
return new Uint8Array([0xfc, ...toLE(value, 2)])
} else if (value < 1 << 32) {
return new Uint8Array([0xfd, ...toLE(value, 4)])
} else if (value < 1 << 64) {
return new Uint8Array([0xfe, ...toLE(value, 8)])
}
}
}
if (type == 'AccountId') {
if (typeof value == 'string') {
return ss58Decode(value);
}
if (value instanceof Uint8Array && value.length == 32) {
return value
}
}
if (typeof value == 'number' || (typeof value == 'string' && +value + '' == value)) {
value = +value
switch (type) {
case 'Balance':
case 'u128':
case 'i128':
return toLE(value, 16)
case 'u64':
case 'i64':
return toLE(value, 8)
case 'AccountIndex':
case 'u32':
case 'i32':
return toLE(value, 4)
case 'u16':
case 'i16':
return toLE(value, 2)
case 'u8':
case 'i8':
return toLE(value, 1)
default:
break
}
}
if (value instanceof AccountIndex && type == 'AccountIndex') {
return toLE(value, 4)
}
if ((value instanceof Perbill || typeof value === 'number') && type == 'Perbill') {
return toLE(value * 1000000000, 4)
}
if ((value instanceof Permill || typeof value === 'number') && type == 'Permill') {
return toLE(value * 1000000, 4)
}
if (value instanceof Uint8Array) {
if (type == 'Signature' && value.length == 64) {
return value
}
if (type == 'Hash' && value.length == 32) {
return value
}
}
if (type == 'TransactionEra' && value instanceof TransactionEra) {
return value.encode()
} else if (type == 'TransactionEra') {
console.error("TxEra::encode bad", type, value)
}
if (type.match(/^<[A-Z][A-Za-z0-9]*asHasCompact>::Type$/) || type.match(/^Compact<[A-Za-z][A-Za-z0-9]*>$/) || type === 'Compact') {
if (value < 1 << 6) {
return new Uint8Array([value << 2])
} else if (value < 1 << 14) {
return toLE((value << 2) + 1, 2)
} else if (value < 1 << 30) {
return toLE((value << 2) + 2, 4)
} else {
let bytes = 0;
for (let v = value; v > 0; v = Math.floor(v / 256)) { ++bytes }
return new Uint8Array([3 + ((bytes - 4) << 2), ...toLE(value, bytes)])
}
}
if (type == 'bool') {
return new Uint8Array([value ? 1 : 0])
}
if (typeof type == 'string' && type.match(/\(.*\)/)) {
return encode(value, type.substr(1, type.length - 2).split(','))
}
// Maybe it's pre-encoded?
if (typeof value == 'object' && value instanceof Uint8Array) {
switch (type) {
case 'Call':
case 'Proposal':
break
default:
console.warn(`Value passed apparently pre-encoded without whitelisting ${type}`)
}
return value
}
throw `Value cannot be encoded as type: ${value}, ${type}`
}
module.exports = { decode, encode, addCodecTransform }