lotus-sdk
Version:
Central repository for several classes of tools for integrating with, and building for, the Lotusia ecosystem
423 lines (422 loc) • 14.2 kB
JavaScript
import { MAX_OP_RETURN_DATA } from '../../utils/constants.js';
import { RNKC } from '../../utils/settings.js';
import { toHex } from '../../utils/functions.js';
import OpCode from './opcode.js';
import { isOpReturn } from './script.js';
export const LOKAD_PREFIX_RANK = 0x52414e4b;
export const LOKAD_PREFIX_RNKC = 0x524e4b43;
export const SCRIPT_CHUNK_LOKAD = new Map();
SCRIPT_CHUNK_LOKAD.set(LOKAD_PREFIX_RANK, 'RANK');
SCRIPT_CHUNK_LOKAD.set(LOKAD_PREFIX_RNKC, 'RNKC');
export const RANK_SENTIMENT_NEUTRAL = OpCode.OP_16;
export const RANK_SENTIMENT_POSITIVE = OpCode.OP_1;
export const RANK_SENTIMENT_NEGATIVE = OpCode.OP_0;
export const SCRIPT_CHUNK_SENTIMENT = new Map();
SCRIPT_CHUNK_SENTIMENT.set(RANK_SENTIMENT_NEUTRAL, 'neutral');
SCRIPT_CHUNK_SENTIMENT.set(RANK_SENTIMENT_POSITIVE, 'positive');
SCRIPT_CHUNK_SENTIMENT.set(RANK_SENTIMENT_NEGATIVE, 'negative');
export const RANK_SENTIMENT_OP_CODES = new Map();
RANK_SENTIMENT_OP_CODES.set('neutral', 'OP_16');
RANK_SENTIMENT_OP_CODES.set('positive', 'OP_1');
RANK_SENTIMENT_OP_CODES.set('negative', 'OP_0');
export const SCRIPT_CHUNK_PLATFORM = new Map();
SCRIPT_CHUNK_PLATFORM.set(0x00, 'lotusia');
SCRIPT_CHUNK_PLATFORM.set(0x01, 'twitter');
export const ScriptChunksRNKCMap = new Map();
ScriptChunksRNKCMap.set('platform', {
offset: 7,
len: 1,
map: SCRIPT_CHUNK_PLATFORM,
});
ScriptChunksRNKCMap.set('profileId', {
offset: 9,
len: null,
});
ScriptChunksRNKCMap.set('postId', {
offset: null,
len: null,
});
ScriptChunksRNKCMap.set('comment', {
offset: null,
len: null,
});
export const RANK_SCRIPT_REQUIRED_LENGTH = 10;
export const ScriptChunksRANKMap = new Map();
ScriptChunksRANKMap.set('sentiment', {
offset: 6,
len: 1,
map: SCRIPT_CHUNK_SENTIMENT,
});
ScriptChunksRANKMap.set('platform', {
offset: 8,
len: 1,
map: SCRIPT_CHUNK_PLATFORM,
});
ScriptChunksRANKMap.set('profileId', {
offset: 10,
len: null,
});
export const ScriptChunksOptionalRANKMap = new Map();
ScriptChunksOptionalRANKMap.set('postId', {
offset: null,
len: null,
});
ScriptChunksOptionalRANKMap.set('postHash', {
offset: null,
len: null,
});
ScriptChunksOptionalRANKMap.set('instanceId', {
offset: null,
len: null,
});
export const PlatformConfiguration = new Map();
PlatformConfiguration.set('lotusia', {
profileId: {
len: 20,
regex: /^[0-9a-fA-F]{40}$/,
},
postId: {
len: 32,
regex: /^[0-9a-f]{64}$/,
type: 'String',
},
});
PlatformConfiguration.set('twitter', {
profileId: {
len: 16,
regex: /^[a-z0-9_]{1,16}$/,
},
postId: {
len: 8,
regex: /^[0-9]+$/,
type: 'BigInt',
},
});
export function toProfileIdBuf(platform, profileId) {
const platformSpec = PlatformConfiguration.get(platform);
if (!platformSpec) {
return null;
}
const profileIdSpec = platformSpec.profileId;
if (!profileIdSpec) {
return null;
}
if (profileIdSpec.regex && !profileIdSpec.regex.test(profileId)) {
return null;
}
const profileBuf = Buffer.alloc(profileIdSpec.len);
switch (platform) {
case 'lotusia': {
const profileIdHex = Buffer.from(profileId, 'hex');
profileBuf.write(profileId, profileIdSpec.len - profileIdHex.length, 'hex');
break;
}
case 'twitter':
profileBuf.write(profileId, profileIdSpec.len - profileId.length, 'utf8');
break;
default:
return null;
}
return profileBuf;
}
export function toProfileIdUTF8(profileIdBuf) {
return new TextDecoder('utf-8').decode(profileIdBuf.filter(byte => byte != 0x00));
}
export function toPostIdBuf(platform, postId) {
switch (platform) {
case 'lotusia':
return Buffer.from(postId, 'hex');
case 'twitter':
return Buffer.from(BigInt(postId).toString(16), 'hex');
default:
return undefined;
}
}
export function toPlatformBuf(platform) {
for (const [byte, platformName] of SCRIPT_CHUNK_PLATFORM) {
if (platformName == platform) {
return Buffer.from([byte]);
}
}
}
export function toPlatformUTF8(platformBuf) {
return SCRIPT_CHUNK_PLATFORM.get(platformBuf.readUint8());
}
export function toSentimentOpCode(sentiment) {
return RANK_SENTIMENT_OP_CODES.get(sentiment);
}
export function toSentimentUTF8(sentimentBuf) {
return SCRIPT_CHUNK_SENTIMENT.get(sentimentBuf.readUInt8());
}
export function toCommentUTF8(commentBuf) {
return new TextDecoder('utf-8').decode(commentBuf);
}
export function toScriptRANK(sentiment, platform, profileId, postId) {
if (!sentiment || !platform || !profileId) {
throw new Error('Must specify sentiment, platform, and profileId');
}
const platformSpec = PlatformConfiguration.get(platform);
if (!platformSpec || !platformSpec.profileId) {
throw new Error('No platform profileId specification defined');
}
const OP_RETURN = toHex(OpCode.OP_RETURN);
const LOKAD_PREFIX = toHex(LOKAD_PREFIX_RANK);
let script = OP_RETURN + toHex(4) + LOKAD_PREFIX;
switch (sentiment) {
case 'neutral':
script += toHex(RANK_SENTIMENT_NEUTRAL);
break;
case 'positive':
script += toHex(RANK_SENTIMENT_POSITIVE);
break;
case 'negative':
script += toHex(RANK_SENTIMENT_NEGATIVE);
break;
}
script += toHex(1) + toHex(toPlatformBuf(platform));
script += toHex(platformSpec.profileId.len);
script += toHex(toProfileIdBuf(platform, profileId));
if (postId) {
if (!platformSpec.postId) {
throw new Error('Post ID provided, but no platform post specification defined');
}
script += toHex(platformSpec.postId.len);
script += toHex(toPostIdBuf(platform, postId));
}
return Buffer.from(script, 'hex');
}
export function toScriptRNKC({ platform, profileId, postId, comment, }) {
if (!platform || !profileId) {
throw new Error('Must specify platform and profileId');
}
const platformSpec = PlatformConfiguration.get(platform);
if (!platformSpec || !platformSpec.profileId) {
throw new Error('No platform profileId specification defined');
}
if (!platformSpec.profileId.regex.test(profileId)) {
throw new Error(`Invalid profileId: ${profileId}`);
}
if (postId && !platformSpec.postId.regex.test(postId)) {
throw new Error(`Invalid postId: ${postId}`);
}
const commentBuf = Buffer.from(comment, 'utf8');
if (commentBuf.length < 1 || commentBuf.length > MAX_OP_RETURN_DATA * 2) {
throw new Error(`Comment must be between 1 and ${MAX_OP_RETURN_DATA * 2} bytes`);
}
const scriptBufs = [];
const OP_RETURN = toHex(OpCode.OP_RETURN);
const OP_PUSHDATA1 = toHex(OpCode.OP_PUSHDATA1);
const LOKAD_PREFIX = toHex(LOKAD_PREFIX_RNKC);
let scriptRNKC = OP_RETURN + toHex(4) + LOKAD_PREFIX;
scriptRNKC += toHex(1) + toHex(toPlatformBuf(platform));
scriptRNKC += toHex(platformSpec.profileId.len);
scriptRNKC += toHex(toProfileIdBuf(platform, profileId));
if (postId) {
scriptRNKC += toHex(platformSpec.postId.len);
scriptRNKC += toHex(toPostIdBuf(platform, postId));
}
scriptBufs.push(Buffer.from(scriptRNKC, 'hex'));
const commentBuf1 = commentBuf.subarray(0, MAX_OP_RETURN_DATA);
let scriptComment = OP_RETURN + OP_PUSHDATA1;
scriptComment += toHex(commentBuf1.length);
scriptComment += toHex(commentBuf1);
scriptBufs.push(Buffer.from(scriptComment, 'hex'));
if (commentBuf.length > MAX_OP_RETURN_DATA) {
const commentBuf2 = commentBuf.subarray(MAX_OP_RETURN_DATA);
let scriptComment2 = OP_RETURN + OP_PUSHDATA1;
scriptComment2 += toHex(commentBuf2.length);
scriptComment2 += toHex(commentBuf2);
scriptBufs.push(Buffer.from(scriptComment2, 'hex'));
}
return scriptBufs;
}
export class ScriptProcessor {
chunks = null;
script;
supplementalScripts = [];
constructor(script) {
this.script = script;
switch (this.lokadType) {
case 'RANK':
this.chunks = ScriptChunksRANKMap;
break;
case 'RNKC':
this.chunks = ScriptChunksRNKCMap;
break;
}
}
addScript(script) {
if (!(script instanceof Buffer)) {
script = Buffer.from(script, 'hex');
}
if (!isOpReturn(script)) {
return false;
}
this.supplementalScripts.push(script);
return true;
}
get lokadType() {
return this.processLokad();
}
processLokad() {
const lokadBuf = this.script.subarray(2, 6);
const lokad = SCRIPT_CHUNK_LOKAD.get(lokadBuf.readUInt32BE(0));
if (!lokad) {
return undefined;
}
return lokad;
}
processSentiment() {
const chunk = this.chunks?.get('sentiment');
if (!chunk || chunk.offset === null) {
return undefined;
}
const sentimentBuf = this.script.subarray(chunk.offset, chunk.offset + chunk.len);
return SCRIPT_CHUNK_SENTIMENT.get(sentimentBuf.readUInt8());
}
processPlatform() {
const chunk = this.chunks?.get('platform');
if (!chunk || chunk.offset === null) {
return undefined;
}
const platformBuf = this.script.subarray(chunk.offset, chunk.offset + chunk.len);
const platform = SCRIPT_CHUNK_PLATFORM.get(platformBuf.readUInt8());
if (!platform) {
return undefined;
}
return platform;
}
processProfileId(platform) {
const chunk = this.chunks?.get('profileId');
if (!chunk || chunk.offset === null) {
return undefined;
}
const platformSpec = PlatformConfiguration.get(platform);
if (!platformSpec || !platformSpec.profileId) {
return undefined;
}
const profileIdSpec = platformSpec.profileId;
const profileIdBuf = this.script.subarray(chunk.offset, chunk.offset + profileIdSpec.len);
if (profileIdBuf.length < profileIdSpec.len) {
return undefined;
}
switch (platform) {
case 'lotusia':
return toHex(profileIdBuf);
case 'twitter':
return toProfileIdUTF8(profileIdBuf);
default:
return undefined;
}
}
processPostId(platform) {
if (!platform) {
return undefined;
}
const platformSpec = PlatformConfiguration.get(platform);
if (!platformSpec || !platformSpec.postId || !platformSpec.profileId) {
return undefined;
}
const profileIdChunk = this.chunks?.get('profileId');
if (!profileIdChunk?.offset) {
return undefined;
}
const postIdSpec = platformSpec.postId;
const postIdOffset = profileIdChunk.offset + platformSpec.profileId.len + 1;
const postIdBuf = this.script.subarray(postIdOffset, postIdOffset + postIdSpec.len);
try {
switch (platform) {
case 'lotusia':
return postIdBuf.toString('hex');
case 'twitter':
return postIdBuf.readBigUInt64BE(0).toString();
default:
return undefined;
}
}
catch (e) {
return undefined;
}
}
processComment(scripts) {
let commentBuf = Buffer.alloc(0);
for (let i = 0; i < scripts.length; i++) {
const script = scripts[i];
if (script.readUInt8(1) !== OpCode.OP_PUSHDATA1) {
break;
}
const dataSize = script.readUInt8(2);
if (isNaN(dataSize) || dataSize > MAX_OP_RETURN_DATA) {
break;
}
commentBuf = Buffer.concat([commentBuf, script.subarray(3, 3 + dataSize)]);
}
if (!commentBuf) {
return null;
}
return new Uint8Array(commentBuf);
}
processScriptRANK() {
const sentiment = this.processSentiment();
if (!sentiment) {
return null;
}
const platform = this.processPlatform();
if (!platform) {
return null;
}
const profileId = this.processProfileId(platform);
if (!profileId) {
return null;
}
const output = {
sentiment,
platform,
profileId,
};
const postId = this.processPostId(platform);
if (postId) {
output.postId = postId;
}
return output;
}
processScriptRNKC(burnedSats) {
if (typeof burnedSats === 'bigint') {
burnedSats = Number(burnedSats);
}
if (this.supplementalScripts.length === 0 ||
this.supplementalScripts.length > 2) {
return null;
}
const inReplyToPlatform = this.processPlatform();
if (!inReplyToPlatform) {
return null;
}
const data = this.processComment(this.supplementalScripts);
if (!data) {
return null;
}
if (data.length < RNKC.minDataLength) {
return null;
}
if (burnedSats < RNKC.minFeeRate * data.length) {
return null;
}
const output = {
data,
feeRate: Math.floor(burnedSats / data.length),
inReplyToPlatform,
inReplyToProfileId: undefined,
inReplyToPostId: undefined,
};
const profileId = this.processProfileId(inReplyToPlatform);
if (profileId) {
output.inReplyToProfileId = profileId;
const postId = this.processPostId(inReplyToPlatform);
if (postId) {
output.inReplyToPostId = postId;
}
}
return output;
}
}