UNPKG

@mora-light/core

Version:
1,655 lines (1,600 loc) 47.2 kB
'use strict'; var principal = require('@dfinity/principal'); var clones = require('./clones-e2de357d.cjs.dev.js'); const NAT_MIN = '0'; const NAT8_MAX = '255'; const NAT16_MAX = '65535'; const NAT32_MAX = '4294967295'; const NAT64_MAX = '18446744073709551615'; const INT8_MAX = '127'; const INT8_MIN = '-128'; const INT16_MAX = '32767'; const INT16_MIN = '-32768'; const INT32_MAX = '2147483647'; const INT32_MIN = '-2147483648'; const INT64_MAX = '9223372036854775807'; const INT64_MIN = '-9223372036854775808'; const NAT_REGEX = '^(0|[1-9][0-9]*)$'; const INT_REGEX = '^(0|[1-9][0-9]*|-[1-9][0-9]*)$'; // ! maybe not exactly correct const FLOAT32_REGEX = '^(0|[+-]?(0|([1-9][0-9]*))(.[0-9]{0,5}[1-9])?|[+-]?[1-9](.[0-9]{0,5}[1-9])?[eE][+-]?([1-9]|[1-2][0-9]|[3][0-8]))$'; const FLOAT64_REGEX = '^(0|[+-]?(0|([1-9][0-9]*))(.[0-9]{0,14}[1-9])?|[+-]?[1-9](.[0-9]{0,14}[1-9])?[eE][+-]?([1-9]|[1-9][0-9]|[12][0-9][0-9]|30[0-8]))$'; // parse candid type array to tuple candid type const parseTuple = types => { const subitems = []; for (let i = 0; i < types.length; i++) subitems[i] = { key: `_${i}_`, type: types[i] }; return { type: 'tuple', subitems }; }; // check text of principal const isPrincipal = text => { try { principal.Principal.fromText(text); } catch (e) { // console.error("principal from text", e); return false; } return true; }; // check text of canister id const isCanisterId = text => { if (!isPrincipal(text)) return false; return text.length === 27; }; // find alone type const findAloneType = type => { if (type.type !== 'tuple') return { type: clones.deepClone(type), child: 0 }; if (type.subitems.length !== 1) return { type: clones.deepClone(type), child: 0 }; const r = findAloneType(type.subitems[0].type); return { type: r.type, child: r.child + 1 }; }; // find alone type const findChildType = (type, child) => { if (child === 0) return clones.deepClone(type); if (type.type !== 'tuple') throw new Error(`can not abstract child type: not tuple candid`); if (type.subitems.length !== 1) throw new Error(`can not abstract child type: not alone`); return findChildType(type.subitems[0].type, child - 1); }; // get empty tuple candid type const getEmptyTupleCandidType = () => { return { type: 'tuple', subitems: [] }; }; // compare two candid type const isSameCandidType = (type1, type2) => isSameCandidTypeInner(type1, type2, [], []); const isSameCandidTypeInner = (type1, type2, rec1, rec2) => { if (!type1) return true; // ! if left type is undefined // ! some types have different type // * situation 1: blob and vec nat8 are same type if (type1.subtype && type2.subtype && isSameCandidType({ type: 'nat8' }, type1.subtype) && isSameCandidType({ type: 'nat8' }, type2.subtype) && ['blob', 'vec'].includes(type1.type) && ['blob', 'vec'].includes(type2.type)) { return true; } // * situation 2: record and variant type actually does not care order of fields if (type1.subitems && type2.subitems && type1.subitems.length === type2.subitems.length && (type1.type === 'record' && type2.type === 'record' || type1.type === 'variant' && type2.type === 'variant')) { // check fields if ((() => { const keys1 = type1.subitems.map(item => item.key); const keys2 = type2.subitems.map(item => item.key); for (const key of keys1) if (!keys2.includes(key)) return false; for (const key of keys2) if (!keys1.includes(key)) return false; for (const key of keys1) { const index1 = type1.subitems.findIndex(subitem => subitem.key === key); const index2 = type2.subitems.findIndex(subitem => subitem.key === key); const subtype1 = type1.subitems[index1].type; const subtype2 = type2.subitems[index2].type; if (!isSameCandidType(subtype1, subtype2)) return false; } return true; })()) return true; } if (type1.type !== type2.type) return false; // main type // subtype if (!clones.isSame(type1.subtype, type2.subtype, (t1, t2) => isSameCandidTypeInner(t1, t2, type1.type !== 'rec' ? rec1 : [...rec1, type1.id], type1.type !== 'rec' ? rec2 : [...rec2, type2.id]))) { return false; } // subitems if (!clones.isSame(type1.subitems, type2.subitems, (items1, items2) => { if (items1.length !== items2.length) return false; // length for (let i = 0; i < items1.length; i++) { if (items1[i].key !== items2[i].key || // key !isSameCandidTypeInner(items1[i].type, items2[i].type, rec1, rec2) // subitem subtype ) { return false; } } return true; })) { return false; } // id if (type1.type !== 'rec') { if (!clones.isSameNumber(type1.id, type2.id)) return false; } else { // recent id if (!clones.isSame(type1.id, type2.id, () => true)) return false; const index1 = rec1.lastIndexOf(type1.id); const index2 = rec2.lastIndexOf(type2.id); if (index1 !== index2) return false; } // func if (!clones.isSame(type1.annotations, type2.annotations)) return false; if (!clones.isSame(type1.argTypes, type2.argTypes, (t1, t2) => isSameCandidTypeInner(t1, t2, rec1, rec2))) return false; if (!clones.isSame(type1.retTypes, type2.retTypes, (t1, t2) => isSameCandidTypeInner(t1, t2, rec1, rec2))) return false; // service if (!clones.isSame(type1.apis, type2.apis, (apis1, apis2) => { if (apis1.length !== apis2.length) return false; // length for (let i = 0; i < apis1.length; i++) { if (apis1[i].method !== apis2[i].method || // method name !isSameCandidTypeInner(apis1[i].func, apis2[i].func, rec1, rec2)) { return false; } } return true; })) { return false; } return true; }; // candid type to string const unwrapCandidType = type => { const isAlphaBeta = key => /^[a-zA-Z_][a-zA-Z_0-9]*$/.test(key); switch (type.type) { case 'bool': case 'nat': case 'int': case 'nat8': case 'nat16': case 'nat32': case 'nat64': case 'int8': case 'int16': case 'int32': case 'int64': case 'float32': case 'float64': case 'null': case 'text': case 'principal': case 'blob': return type.type; case 'vec': case 'opt': return `${type.type} ${unwrapCandidType(type.subtype)}`; case 'record': case 'variant': if (type.subitems.length === 0) return `${type.type} {}`; return `${type.type} { ${type.subitems.map(subitem => { let key = isAlphaBeta(subitem.key) ? `${subitem.key}` : `"${subitem.key.replaceAll('"', '\\"')}`; return isSameCandidType(subitem.type, { type: 'null' }) ? key : `${key}: ${unwrapCandidType(subitem.type)}`; }).join('; ')} }`; case 'tuple': if (type.subitems.length === 0) return `record {}`; return `record { ${type.subitems.map(subitem => `${unwrapCandidType(subitem.type)}`).join('; ')} }`; case 'rec': if (type.subtype === undefined) return `rec_${type.id}`; return `μrec_${type.id}.${unwrapCandidType(type.subtype)}`; case 'unknown': case 'empty': case 'reserved': return type.type; case 'func': return `(${type.argTypes.subitems.map(subitem => unwrapCandidType(subitem.type)).join(', ')}) -> (${type.retTypes.subitems.map(subitem => unwrapCandidType(subitem.type)).join(', ')})${type.annotations.length ? ' query' : ''}`; case 'service': if (type.apis.length === 0) return `service: {}`; return `service: { ${type.apis.map(api => { if (isAlphaBeta(api.method)) { return `${api.method}: ${unwrapCandidType(api.func)}`; } else { return `"${api.method.replaceAll('"', '\\"')}": ${unwrapCandidType(api.func)}`; } })} }`; } }; // string to candid type // ! maybe there are bugs here const wrapCandidType = text => { try { // if (text === 'vec nat8') debugger; const { type, remained } = wrapCandidType0({ text, index: -1 }); if (remained.text.length === 0) return { ok: type }; throw new Error(`wrong type from position: ${remained.index} | ${remained.text}`); } catch (e) { console.error('wrapCandidType failed', text, e); return { err: e.message }; } }; const wrapCandidType0 = data => { const EMPTY_CHAR = [' ', '\n', '\r']; const trimStart = data => { let text = data.text; let index = data.index; while (text.length > 0 && EMPTY_CHAR.includes(text.charAt(0))) { index += 1; text = text.substring(1); } return { text, index }; }; // try to read a candid type // console.error('wrapCandidType0', data.text, data.index); // 1. trim first blanks data = trimStart(data); // 2. empty is wrong if (data.text.length === 0) { throw new Error(`wrong type from position: ${data.index} | ${data.text}`); } // 3. simple type const simpleType = type => { if (data.text.startsWith(type.type)) { const text = data.text.substring(type.type.length); const index = data.index + type.type.length; const next = text.length === 0 ? '' : text.substring(0, 1); if (next === '' || EMPTY_CHAR.includes(next) || ['{', '(', ';', '}'].includes(next)) { let data = { text, index }; data = trimStart(data); return { type, remained: data }; } } return undefined; }; let r = undefined; if ((r = simpleType({ type: 'bool' })) !== undefined) return r; if ((r = simpleType({ type: 'nat' })) !== undefined) return r; if ((r = simpleType({ type: 'int' })) !== undefined) return r; if ((r = simpleType({ type: 'nat8' })) !== undefined) return r; if ((r = simpleType({ type: 'nat16' })) !== undefined) return r; if ((r = simpleType({ type: 'nat32' })) !== undefined) return r; if ((r = simpleType({ type: 'nat64' })) !== undefined) return r; if ((r = simpleType({ type: 'int8' })) !== undefined) return r; if ((r = simpleType({ type: 'int16' })) !== undefined) return r; if ((r = simpleType({ type: 'int32' })) !== undefined) return r; if ((r = simpleType({ type: 'int64' })) !== undefined) return r; if ((r = simpleType({ type: 'float32' })) !== undefined) return r; if ((r = simpleType({ type: 'float64' })) !== undefined) return r; if ((r = simpleType({ type: 'null' })) !== undefined) return r; if ((r = simpleType({ type: 'text' })) !== undefined) return r; if ((r = simpleType({ type: 'principal' })) !== undefined) return r; if ((r = simpleType({ type: 'blob', subtype: { type: 'nat8' } })) !== undefined) return r; if ((r = simpleType({ type: 'unknown' })) !== undefined) return r; if ((r = simpleType({ type: 'empty' })) !== undefined) return r; if ((r = simpleType({ type: 'reserved' })) !== undefined) return r; // vec and opt have subtype if (data.text.startsWith('vec') || data.text.startsWith('opt')) { const main = data.text.substring(0, 3); const text = data.text.substring(main.length); const index = data.index + main.length; const next = text.length === 0 ? '' : text.substring(0, 1); if (next === ' ') { // next char must be blank let data = { text, index }; data = trimStart(data); // try to read subtype let { type: subtype, remained } = wrapCandidType0(data); remained = trimStart(remained); const type = { type: main, subtype: subtype }; return { type, remained }; } } const readKey = text => { if (text.length === 0) return ['', 0]; const next = text.substring(0, 1); if (/^[a-zA-Z_]$/.test(next)) { let key = next; text = text.substring(1); while (true) { if (text.length === 0) return ['', 0]; const next = text.substring(0, 1); if ([' ', ':', ';', '}'].includes(next)) break; key = key + next; text = text.substring(1); } return [key, key.length]; } else if (next === '"') { let key = ''; let count = 0; text = text.substring(1); while (true) { if (text.length === 0) return ['', 0]; const next = text.substring(0, 1); if (next === '\\') { const next2 = text.length >= 2 ? text.substring(1, 2) : ''; const CHARS = [['b', '\b'], ['t', '\t'], ['n', '\n'], ['v', '\v'], ['f', '\f'], ['r', '\r'], ['"', '"'], ["'", "'"], ['\\', '\\']]; const item = CHARS.find(cs => cs[0] === next2); if (item !== undefined) { key = key + item[1]; text = text.substring(2); count += 2; continue; } return ['', 0]; } if (next === '"') { text = text.substring(1); return [key, count + 2]; } key = key + next; count += 1; text = text.substring(1); } } return ['', 0]; }; // variant if (data.text.startsWith('variant')) { const main = data.text.substring(0, 7); const text = data.text.substring(main.length); const index = data.index + main.length; if (text.length) { // must has something let data = { text, index }; data = trimStart(data); // next char must be { if (data.text.startsWith('{')) { data = { text: data.text.substring(1), index: data.index + 1 }; // try to read subitem const subitems = []; while (true) { data = trimStart(data); if (data.text.length === 0) break; if (data.text.startsWith('}')) break; const [key, length] = readKey(data.text); if (key.length === 0) break; let temp = { text: data.text.substring(length), index: data.index + length }; temp = trimStart(temp); if (temp.text.startsWith('}')) { subitems.push({ key, type: { type: 'null' } }); data = temp; break; } if (temp.text.startsWith(';')) { subitems.push({ key, type: { type: 'null' } }); } else { if (!temp.text.startsWith(':')) break; temp = { text: temp.text.substring(1), index: temp.index + 1 }; temp = trimStart(temp); let { type, remained } = wrapCandidType0(temp); subitems.push({ key, type }); temp = remained; } let remained = trimStart(temp); if (remained.text.startsWith(';')) remained = { text: remained.text.substring(1), index: remained.index + 1 }; data = remained; } if (data.text.startsWith('}')) { const type = { type: main, subitems }; data = { text: data.text.substring(1), index: data.index + 1 }; data = trimStart(data); return { type, remained: data }; } } } } // record if (data.text.startsWith('record')) { const main = data.text.substring(0, 6); const text = data.text.substring(main.length); const index = data.index + main.length; if (text.length) { // must has something let data = { text, index }; data = trimStart(data); // next char must be { if (data.text.startsWith('{')) { data = { text: data.text.substring(1), index: data.index + 1 }; // try to read subitem const subitems = []; while (true) { data = trimStart(data); if (data.text.length === 0) break; if (data.text.startsWith('}')) break; const [key, length] = readKey(data.text); if (key.length === 0) break; let temp = { text: data.text.substring(length), index: data.index + length }; temp = trimStart(temp); if (!temp.text.startsWith(':')) break; temp = { text: temp.text.substring(1), index: temp.index + 1 }; temp = trimStart(temp); let { type, remained } = wrapCandidType0(temp); subitems.push({ key, type }); remained = trimStart(remained); if (remained.text.startsWith(';')) remained = { text: remained.text.substring(1), index: remained.index + 1 }; data = remained; } if (data.text.startsWith('}')) { const type = { type: main, subitems }; data = { text: data.text.substring(1), index: data.index + 1 }; data = trimStart(data); return { type, remained: data }; } } } } // tuple if (data.text.startsWith('record') || data.text.startsWith('tuple')) { const main = data.text.substring(0, data.text.startsWith('record') ? 6 : 5); const text = data.text.substring(main.length); const index = data.index + main.length; if (text.length) { // must has something let data = { text, index }; data = trimStart(data); // next char must be { if (data.text.startsWith('{')) { data = { text: data.text.substring(1), index: data.index + 1 }; // try to read subitem const subitems = []; while (true) { data = trimStart(data); if (data.text.length === 0) break; if (data.text.startsWith('}')) break; let { type, remained } = wrapCandidType0(data); subitems.push({ key: `_${subitems.length}_`, type }); remained = trimStart(remained); if (remained.text.startsWith(';')) remained = { text: remained.text.substring(1), index: remained.index + 1 }; data = remained; } if (data.text.startsWith('}')) { const type = { type: 'tuple', subitems }; data = { text: data.text.substring(1), index: data.index + 1 }; data = trimStart(data); return { type, remained: data }; } } } } // rec if (/^μrec_\d+\./.test(data.text)) { // rec start let text = data.text.substring(5); let index = data.index + 5; const id = parseInt(text.substring(0, text.indexOf('.'))); index = index + text.indexOf('.') + 1; text = text.substring(text.indexOf('.') + 1); let { type, remained } = wrapCandidType0({ text, index }); remained = trimStart(remained); return { type: { type: 'rec', subtype: type, id }, remained }; } if (/^rec_\d+/.test(data.text)) { // rec end let text = data.text.substring(4); let index = data.index + 4; let m = text.match(/^\d+/); if (m && m[0].length) { const id = parseInt(m[0]); text = text.substring(m[0].length); index = index + m[0].length; let remained = { text, index }; remained = trimStart(remained); const type = { type: 'rec', id }; return { type, remained }; } } // func const findFunc = data => { if (data.text.startsWith('(')) { const readTuple = (text, index) => { if (!text.startsWith('(')) return undefined; text = text.substring(1); index = index + 1; let data = { text, index }; const subitems = []; while (true) { data = trimStart(data); if (data.text.length === 0) break; if (data.text.startsWith(')')) break; let { type, remained } = wrapCandidType0(data); subitems.push({ key: `_${subitems.length}_`, type }); remained = trimStart(remained); if (remained.text.startsWith(',')) remained = { text: remained.text.substring(1), index: remained.index + 1 }; data = remained; } if (data.text.startsWith(')')) { const type = { type: 'tuple', subitems }; data = { text: data.text.substring(1), index: data.index + 1 }; data = trimStart(data); return [type, data]; } }; const arg = readTuple(data.text, data.index); if (arg !== undefined) { const argTypes = arg[0]; let data = arg[1]; // must be -> if (data.text.startsWith('->')) { data = { text: data.text.substring(2), index: data.index + 2 }; data = trimStart(data); const ret = readTuple(data.text, data.index); if (ret !== undefined) { const retTypes = ret[0]; let data = ret[1]; data = trimStart(data); let annotations = []; if (data.text.startsWith('query')) { annotations = ['query']; data = { text: data.text.substring(5), index: data.index + 5 }; data = trimStart(data); } const type = { type: 'func', annotations, argTypes, retTypes }; return { type, remained: data }; } } } } return undefined; }; const func = findFunc({ ...data }); if (func !== undefined) return func; // service if (data.text.startsWith('service')) { const text = data.text.substring(7); const index = data.index + 7; if (text.length) { let data = { text, index }; data = trimStart(data); if (data.text.startsWith(':')) { const text = data.text.substring(1); const index = data.index + 1; data = { text, index }; data = trimStart(data); if (data.text.startsWith('{')) { data = { text: data.text.substring(1), index: data.index + 1 }; const apis = []; const findKey = text => { if (text.length === 0) return ['', 0]; const next = text.substring(0, 1); if (/^[a-zA-Z_]$/.test(next)) { let key = next; text = text.substring(0, 1); while (true) { if (text.length === 0) return ['', 0]; const next = text.substring(0, 1); if ([' ', ':'].includes(next)) break; key = key + next; text = text.substring(1); } return [key, key.length]; } return ['', 0]; }; while (true) { data = trimStart(data); if (data.text.length === 0) break; if (data.text.startsWith('}')) break; const [key, length] = findKey(data.text); if (key.length === 0) break; let temp = { text: data.text.substring(length), index: data.index + length }; temp = trimStart(temp); if (!temp.text.startsWith(':')) break; temp = { text: temp.text.substring(1), index: temp.index + 1 }; temp = trimStart(temp); const func = findFunc(temp); if (func === undefined) break; apis.push({ method: key, func: func.type }); let remained = trimStart(func.remained); if (remained.text.startsWith(';')) remained = { text: remained.text.substring(1), index: remained.index + 1 }; data = remained; } if (data.text.startsWith('}')) { const type = { type: 'service', apis }; data = { text: data.text.substring(1), index: data.index + 1 }; data = trimStart(data); return { type, remained: data }; } } } } } // can not read type throw new Error(`wrong type from position: ${data.index} | ${data.text}`); }; // stringify candid type const stringifyCandidType = type => { const clean = t => { switch (t.type) { case 'bool': case 'nat': case 'int': case 'nat8': case 'nat16': case 'nat32': case 'nat64': case 'int8': case 'int16': case 'int32': case 'int64': case 'float32': case 'float64': case 'null': case 'text': case 'principal': return { type: t.type }; case 'blob': return { type: t.type, subtype: { type: 'nat8' } }; case 'vec': return { type: t.type, subtype: clean(t.subtype) }; case 'opt': return { type: t.type, subtype: clean(t.subtype) }; case 'record': return { type: t.type, subitems: t.subitems.map(subitem => { return { key: subitem.key, type: clean(subitem.type) }; }) }; case 'variant': return { type: t.type, subitems: t.subitems.map(subitem => { return { key: subitem.key, type: clean(subitem.type) }; }) }; case 'tuple': return { type: t.type, subitems: t.subitems.map(subitem => { return { key: subitem.key, type: clean(subitem.type) }; }) }; case 'rec': return t.subtype !== undefined ? { type: t.type, subtype: clean(t.subtype), id: t.id } : { type: t.type, subtype: undefined, id: t.id }; case 'unknown': case 'empty': case 'reserved': return { type: t.type }; case 'func': return { type: t.type, annotations: [...t.annotations], argTypes: clean(t.argTypes), retTypes: clean(t.retTypes) }; case 'service': return { type: t.type, apis: t.apis.map(api => { return { method: api.method, func: clean(api.func) }; }) }; } }; const t = clean(clones.deepClone(type)); return JSON.stringify(t); }; // initial value for candid type const getInitialCandidTypeValue = (type, recItems, requested) => { const subitems = type.subitems ?? []; switch (type.type) { case 'bool': return false; case 'nat': case 'int': return { type: 'bigint', value: '0' }; case 'nat8': case 'nat16': case 'nat32': return 0; case 'nat64': return { type: 'bigint', value: '0' }; case 'int8': case 'int16': case 'int32': return 0; case 'int64': return { type: 'bigint', value: '0' }; case 'float32': case 'float64': return 0.0; case 'null': return null; case 'text': return ''; case 'principal': return { type: 'principal', value: '' }; // ! it is not a correct value case 'blob': return []; case 'vec': return []; case 'opt': return []; // none case 'record': const record = {}; subitems.forEach(subitem => record[subitem.key] = getInitialCandidTypeValue(subitem.type, recItems, requested)); // every key has value return record; case 'variant': const variant = {}; for (let i = 0; i < subitems.length; i++) { variant[subitems[i].key] = getInitialCandidTypeValue(subitems[i].type, recItems, requested); break; // one is enough } return variant; case 'tuple': return subitems.map(subitem => getInitialCandidTypeValue(subitem.type, recItems, requested)); // every key has value case 'rec': const request = stringifyCandidType(type); if (requested.includes(request)) { console.error('recursive object can not be instanced', requested, request, type); throw new Error(`recursive object can not be instanced: ${stringifyCandidType(type)}`); } requested = [...requested, request]; const { subtype: recSubtype, recItems: recItems2 } = findRecSubtype(type, recItems); return getInitialCandidTypeValue(recSubtype, recItems2, requested); case 'unknown': throw new Error(`can not instanced for type 'unknown'`); case 'empty': throw new Error(`can not instanced for type 'empty'`); case 'reserved': return null; case 'func': return { type: 'func', value: { service: '', // ! it is not a correct value method: '' // ! it is not a correct value } }; case 'service': return { type: 'service', value: '' // ! it is not a correct value }; } }; const findRecType = (recItems, id) => { for (let i = recItems.length - 1; 0 <= i; i--) { if (recItems[i].id === id) return recItems[i].type; } return undefined; }; const findRecSubtype = (type, recItems) => { const found = findRecType(recItems, type.id); let recSubType; if (found !== undefined) { if (type.subtype !== undefined) { recSubType = type.subtype; recItems = [...recItems, { id: type.id, type }]; } else { recSubType = found.subtype; } } else { if (type.subtype !== undefined) { recSubType = type.subtype; recItems = [...recItems, { id: type.id, type }]; } else { throw new Error(`wrong recursive type: ${stringifyCandidType(type)}`); } } return { subtype: recSubType, recItems }; }; const findChildTypeAndValue = (value, from, child) => { // 1. check type and values if (!checkCandidValue(from, value, [])) { throw new Error(`value: ${JSON.stringify(value)} is not match type: ${stringifyCandidType(from)}`); } if (child === undefined || child === 0) return { type: from, value }; // does not need fetch child // 2. check child level if (findAloneType(from).child !== child) { throw new Error(`type: ${stringifyCandidType(from)} and child: ${child} is not matched`); } // 3. fetch child while (child > 0) { from = from.subitems[0].type; value = value[0]; child--; } // 4. return value return { type: from, value }; }; // ================== check value and type ================== const isArray = value => { if (value === undefined) return false; if (value === null) return false; const flag = Object.prototype.toString.call(value) === '[object Array]'; if (flag) return flag; if (typeof value !== 'object') return false; // some array like {length: 1, 0: 1}, obviously it is object let length = value['length']; if (!['number', 'undefined'].includes(typeof length)) return false; const keys = Object.keys(value).filter(key => key !== 'length').map(key => parseInt(key)); if (typeof length === 'number' && length !== keys.length) return false; length = keys.length; for (let i = 0; i < length; i++) { const index = keys.indexOf(i); if (index >= 0) keys.splice(index, 1); } if (keys.length) return false; value['length'] = length; // add length filed return true; }; const isBool = value => typeof value === 'boolean'; const isNat = value => typeof value === 'object' && value['type'] === 'bigint' && value['value'].match(NAT_REGEX); const isInt = value => typeof value === 'object' && value['type'] === 'bigint' && value['value'].match(INT_REGEX); const isNat8 = value => typeof value === 'number' && Number(NAT_MIN) <= value && value <= Number(NAT8_MAX); const isNat16 = value => typeof value === 'number' && Number(NAT_MIN) <= value && value <= Number(NAT16_MAX); const isNat32 = value => typeof value === 'number' && Number(NAT_MIN) <= value && value <= Number(NAT32_MAX); const isNat64 = value => { if (!isNat(value)) return false; const v = BigInt(value['value']); return BigInt(`${NAT_MIN}`) <= v && v <= BigInt(NAT64_MAX); }; const isInt8 = value => typeof value === 'number' && Number(INT8_MIN) <= value && value <= Number(INT8_MAX); const isInt16 = value => typeof value === 'number' && Number(INT16_MIN) <= value && value <= Number(INT16_MAX); const isInt32 = value => typeof value === 'number' && Number(INT32_MIN) <= value && value <= Number(INT32_MAX); const isInt64 = value => { if (!isInt(value)) return false; const v = BigInt(value['value']); return BigInt(INT64_MIN) <= v && v <= BigInt(INT64_MAX); }; const isFloat32 = value => // typeof value === "number" && !!`${value}`.match(FLOAT32_REGEX); typeof value === 'number'; // ? decimal ??? const isFloat64 = value => // typeof value === "number" && !!`${value}`.match(FLOAT64_REGEX); typeof value === 'number'; // ? decimal ??? const isNull = value => value === null; const isText = value => typeof value === 'string'; const isCandidPrincipal = value => typeof value === 'object' && value['type'] === 'principal' && isPrincipal(value['value']); const isBlob = value => { if (!isArray(value)) return false; const length = value['length']; if (typeof length !== 'number') return false; for (let i = 0; i < length; i++) if (!isNat8(value[i])) return false; return true; }; const isVec = (value, subtype, recItems) => { if (!isArray(value)) return false; const length = value['length']; if (typeof length !== 'number') return false; for (let i = 0; i < length; i++) if (!checkCandidValue(subtype, value[i], recItems)) return false; return true; }; const isOpt = (value, subtype, recItems) => { if (!isArray(value)) return false; const length = value['length']; if (typeof length !== 'number') return false; switch (length) { case 0: return true; case 1: return checkCandidValue(subtype, value[0], recItems); } return false; }; const isRecord = (value, subitems, recItems) => { if (typeof value !== 'object') return false; const valueKeys = Object.keys(value); if (valueKeys.length !== subitems.length) return false; for (let i = 0; i < subitems.length; i++) { const subitem = subitems[i]; if (!checkCandidValue(subitem.type, value[subitem.key], recItems)) return false; valueKeys.splice(valueKeys.findIndex(key => key === subitem.key), 1); } return valueKeys.length === 0; }; const isVariant = (value, subitems, recItems) => { if (typeof value !== 'object') return false; const valueKeys = Object.keys(value); if (valueKeys.length > 1) return false; if (subitems.length > 0) { if (valueKeys.length > 0) { const key = valueKeys[0]; // the only key const findSubitems = subitems.filter(subitem => subitem.key === key); // find the chosen key if (findSubitems.length !== 1) return false; const subitem = findSubitems[0]; return checkCandidValue(subitem.type, value[key], recItems); } else { return false; } } else { if (valueKeys.length > 0) { return false; } else { return true; } } }; const isTuple = (value, subitems, recItems) => { if (!isArray(value)) return false; const length = value['length']; if (typeof length !== 'number') return false; if (length !== subitems.length) return false; for (let i = 0; i < length; i++) { if (!checkCandidValue(subitems[i].type, value[i], recItems)) return false; } return true; }; const isCandidFunc = value => typeof value === 'object' && value['type'] === 'func' && typeof value['value'] === 'object' && isCanisterId(value['value']['service']) && value['value']['method']; // ? method ??? const isCandidService = value => typeof value === 'object' && value['type'] === 'service' && isCanisterId(value['value']); const checkCandidValue = (type, value, recItems) => { if (value === undefined) return false; // console.error("checkCandidValue", type, value); switch (type.type) { case 'bool': return isBool(value); case 'nat': return isNat(value); case 'int': return isInt(value); case 'nat8': return isNat8(value); case 'nat16': return isNat16(value); case 'nat32': return isNat32(value); case 'nat64': return isNat64(value); case 'int8': return isInt8(value); case 'int16': return isInt16(value); case 'int32': return isInt32(value); case 'int64': return isInt64(value); case 'float32': return isFloat32(value); case 'float64': return isFloat64(value); case 'null': return isNull(value); case 'text': return isText(value); case 'principal': return isCandidPrincipal(value); case 'blob': return isBlob(value); case 'vec': return isVec(value, type.subtype, recItems); case 'opt': return isOpt(value, type.subtype, recItems); case 'record': return isRecord(value, type.subitems, recItems); case 'variant': return isVariant(value, type.subitems, recItems); case 'tuple': return isTuple(value, type.subitems, recItems); case 'rec': const { subtype: recSubtype, recItems: recItems2 } = findRecSubtype(type, recItems); return checkCandidValue(recSubtype, value, recItems2); case 'unknown': return false; case 'empty': return false; case 'reserved': return true; // any type case 'func': return isCandidFunc(value); case 'service': return isCandidService(value); } }; const checkCandidTypeAndCandidValue = (type, value, recItems, el) => { if (!checkCandidValue(type, value, recItems)) { return { err: { message: `value: ${JSON.stringify(value)} is not match type: ${stringifyCandidType(type)}`, el } }; } return { ok: value }; }; // ================== wrap value ================== // unwrap candid value // * CandidBigInt -> BigInt // * CandidPrincipal -> Principal const unwrapCandidValue = (type, value, recItems) => { if (value === undefined) throw new Error('candid value can not be undefined'); if (!checkCandidValue(type, value, [])) { throw new Error(`value: ${JSON.stringify(value)} is not match type: ${stringifyCandidType(type)}`); } let v = value; switch (type.type) { case 'bool': break; case 'nat': case 'int': v = BigInt(v['value']); break; case 'nat8': case 'nat16': case 'nat32': break; case 'nat64': v = BigInt(v['value']); break; case 'int8': case 'int16': case 'int32': break; case 'int64': v = BigInt(v['value']); break; case 'float32': case 'float64': break; case 'null': break; case 'text': break; case 'principal': v = principal.Principal.fromText(v['value']); // ! real type break; case 'blob': break; case 'vec': for (let i = 0; i < v['length']; i++) v[i] = unwrapCandidValue(type.subtype, v[i], recItems); break; case 'opt': for (let i = 0; i < v['length']; i++) v[i] = unwrapCandidValue(type.subtype, v[i], recItems); break; case 'record': for (let i = 0; i < type.subitems.length; i++) { const item = type.subitems[i]; v[item.key] = unwrapCandidValue(item.type, v[item.key], recItems); } break; case 'variant': const variantValueKeys = Object.keys(v); if (variantValueKeys.length) { const key = variantValueKeys[0]; for (let i = 0; i < type.subitems.length; i++) { if (key === type.subitems[i].key) { v[key] = unwrapCandidValue(type.subitems[i].type, v[key], recItems); break; } } } break; case 'tuple': for (let i = 0; i < type.subitems.length; i++) v[i] = unwrapCandidValue(type.subitems[i].type, v[i], recItems); break; case 'rec': const { subtype: recSubtype, recItems: recItems2 } = findRecSubtype(type, recItems); return unwrapCandidValue(recSubtype, value, recItems2); case 'unknown': case 'empty': case 'reserved': break; case 'func': // actually, it is [ Principal, string ] v = [principal.Principal.fromText(v['value']['service']), v['value']['method']]; break; case 'service': // actually, it is Principal v = principal.Principal.fromText(v['value']); // ! real type break; } return v; }; // wrap value // * BigInt -> CandidBigInt // * Principal -> CandidPrincipal const wrapCandidValue = (type, value, recItems) => { if (value === undefined) throw new Error('candid value can not be undefined'); let v = value; switch (type.type) { case 'bool': break; case 'nat': case 'int': v = { type: 'bigint', value: `${v}` }; break; case 'nat8': case 'nat16': case 'nat32': break; case 'nat64': v = { type: 'bigint', value: `${v}` }; break; case 'int8': case 'int16': case 'int32': break; case 'int64': v = { type: 'bigint', value: `${v}` }; break; case 'float32': case 'float64': break; case 'null': break; case 'text': break; case 'principal': v = { type: 'principal', value: value.toText() }; // ! to text break; case 'blob': break; case 'vec': for (let i = 0; i < v['length']; i++) v[i] = wrapCandidValue(type.subtype, v[i], recItems); break; case 'opt': for (let i = 0; i < v['length']; i++) v[i] = wrapCandidValue(type.subtype, v[i], recItems); break; case 'record': for (let i = 0; i < type.subitems.length; i++) { const item = type.subitems[i]; v[item.key] = wrapCandidValue(item.type, v[item.key], recItems); } break; case 'variant': const variantValueKeys = Object.keys(v); if (variantValueKeys.length) { const key = variantValueKeys[0]; for (let i = 0; i < type.subitems.length; i++) { if (key === type.subitems[i].key) { v[key] = wrapCandidValue(type.subitems[i].type, v[key], recItems); break; } } } break; case 'tuple': for (let i = 0; i < type.subitems.length; i++) v[i] = wrapCandidValue(type.subitems[i].type, v[i], recItems); break; case 'rec': const { subtype: recSubtype, recItems: recItems2 } = findRecSubtype(type, recItems); return wrapCandidValue(recSubtype, value, recItems2); case 'unknown': case 'empty': case 'reserved': break; case 'func': // actually, it is [ Principal, string ] v = { type: 'func', value: { service: principal.Principal.fromUint8Array(parseUint8Array(value[0]['_arr'])).toText(), method: value[1] } }; break; case 'service': // actually, it is Principal v = { type: 'service', value: principal.Principal.fromUint8Array(parseUint8Array(value['_arr'])).toText() // ! to text }; break; } return v; }; // for principal const parseUint8Array = arr => { if (!isArray(arr)) throw new Error('arr is not array'); const value = new Uint8Array(arr.length); for (let i = 0; i < arr.length; i++) { value[i] = arr[i]; } return value; }; exports.FLOAT32_REGEX = FLOAT32_REGEX; exports.FLOAT64_REGEX = FLOAT64_REGEX; exports.INT16_MAX = INT16_MAX; exports.INT16_MIN = INT16_MIN; exports.INT32_MAX = INT32_MAX; exports.INT32_MIN = INT32_MIN; exports.INT64_MAX = INT64_MAX; exports.INT64_MIN = INT64_MIN; exports.INT8_MAX = INT8_MAX; exports.INT8_MIN = INT8_MIN; exports.INT_REGEX = INT_REGEX; exports.NAT16_MAX = NAT16_MAX; exports.NAT32_MAX = NAT32_MAX; exports.NAT64_MAX = NAT64_MAX; exports.NAT8_MAX = NAT8_MAX; exports.NAT_MIN = NAT_MIN; exports.NAT_REGEX = NAT_REGEX; exports.checkCandidTypeAndCandidValue = checkCandidTypeAndCandidValue; exports.checkCandidValue = checkCandidValue; exports.findAloneType = findAloneType; exports.findChildType = findChildType; exports.findChildTypeAndValue = findChildTypeAndValue; exports.findRecSubtype = findRecSubtype; exports.findRecType = findRecType; exports.getEmptyTupleCandidType = getEmptyTupleCandidType; exports.getInitialCandidTypeValue = getInitialCandidTypeValue; exports.isArray = isArray; exports.isBlob = isBlob; exports.isBool = isBool; exports.isCandidFunc = isCandidFunc; exports.isCandidPrincipal = isCandidPrincipal; exports.isCandidService = isCandidService; exports.isCanisterId = isCanisterId; exports.isFloat32 = isFloat32; exports.isFloat64 = isFloat64; exports.isInt = isInt; exports.isInt16 = isInt16; exports.isInt32 = isInt32; exports.isInt64 = isInt64; exports.isInt8 = isInt8; exports.isNat = isNat; exports.isNat16 = isNat16; exports.isNat32 = isNat32; exports.isNat64 = isNat64; exports.isNat8 = isNat8; exports.isNull = isNull; exports.isOpt = isOpt; exports.isPrincipal = isPrincipal; exports.isRecord = isRecord; exports.isSameCandidType = isSameCandidType; exports.isText = isText; exports.isTuple = isTuple; exports.isVariant = isVariant; exports.isVec = isVec; exports.parseTuple = parseTuple; exports.stringifyCandidType = stringifyCandidType; exports.unwrapCandidType = unwrapCandidType; exports.unwrapCandidValue = unwrapCandidValue; exports.wrapCandidType = wrapCandidType; exports.wrapCandidValue = wrapCandidValue;