UNPKG

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
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; } }