mnee
Version:
A simple package for interacting with the MNEE USD
182 lines (156 loc) • 5.52 kB
text/typescript
import { Hash, OP, Script, Transaction, Utils } from '@bsv/sdk';
import {
Inscription,
MNEEConfig,
MneeInscription,
MneeSync,
ParsedCosigner,
TxHistory,
TxStatus,
TxType,
} from '../mnee.types';
export const parseInscription = (script: Script) => {
let fromPos: number | undefined;
for (let i = 0; i < script.chunks.length; i++) {
const chunk = script.chunks[i];
if (
i >= 2 &&
chunk.data?.length === 3 &&
Utils.toUTF8(chunk.data) == 'ord' &&
script.chunks[i - 1].op == OP.OP_IF &&
script.chunks[i - 2].op == OP.OP_FALSE
) {
fromPos = i + 1;
}
}
if (fromPos === undefined) return;
const insc = {
file: { hash: '', size: 0, type: '' },
fields: {},
} as Inscription;
for (let i = fromPos; i < script.chunks.length; i += 2) {
const field = script.chunks[i];
if (field.op == OP.OP_ENDIF) {
break;
}
if (field.op > OP.OP_16) return;
const value = script.chunks[i + 1];
if (value.op > OP.OP_PUSHDATA4) return;
if (field.data?.length) continue;
let fieldNo = 0;
if (field.op > OP.OP_PUSHDATA4 && field.op <= OP.OP_16) {
fieldNo = field.op - 80;
} else if (field.data?.length) {
fieldNo = field.data[0];
}
switch (fieldNo) {
case 0:
insc.file!.size = value.data?.length || 0;
if (!value.data?.length) break;
insc.file!.hash = Utils.toBase64(Hash.sha256(value.data));
insc.file!.content = value.data;
break;
case 1:
insc.file!.type = Buffer.from(value.data || []).toString();
break;
}
}
return insc;
};
export const parseCosignerScripts = (scripts: any): ParsedCosigner[] => {
return scripts.map((script: any) => {
const chunks = script.chunks;
for (let i = 0; i <= chunks.length - 4; i++) {
if (
chunks.length > i + 6 &&
chunks[0 + i].op === OP.OP_DUP &&
chunks[1 + i].op === OP.OP_HASH160 &&
chunks[2 + i].data?.length === 20 &&
chunks[3 + i].op === OP.OP_EQUALVERIFY &&
chunks[4 + i].op === OP.OP_CHECKSIGVERIFY &&
chunks[5 + i].data?.length === 33 &&
chunks[6 + i].op === OP.OP_CHECKSIG
) {
return {
cosigner: Utils.toHex(chunks[5 + i].data || []),
address: Utils.toBase58Check(chunks[2 + i].data || [], [0]),
};
} else if (
// P2PKH
chunks[0 + i].op === OP.OP_DUP &&
chunks[1 + i].op === OP.OP_HASH160 &&
chunks[2 + i].data?.length === 20 &&
chunks[3 + i].op === OP.OP_EQUALVERIFY &&
chunks[4 + i].op === OP.OP_CHECKSIG
) {
return {
cosigner: '',
address: Utils.toBase58Check(chunks[2 + i].data || [], [0]),
};
}
}
});
};
export const parseSyncToTxHistory = (sync: MneeSync, address: string, config: MNEEConfig): TxHistory | null => {
const txType: TxType = sync.senders.includes(address) ? 'send' : 'receive';
const txStatus: TxStatus = sync.height > 0 ? 'confirmed' : 'unconfirmed';
if (!sync.rawtx) return null;
const txArray = Utils.toArray(sync.rawtx, 'base64');
const txHex = Utils.toHex(txArray);
const tx = Transaction.fromHex(txHex);
const outScripts = tx.outputs.map((output) => output.lockingScript);
const mneeScripts = parseCosignerScripts(outScripts);
const parsedOutScripts = outScripts.map(parseInscription);
const mneeAddresses = mneeScripts.map((script) => script.address);
const feeAddressIndex = mneeAddresses.indexOf(config.feeAddress);
const sender = sync.senders[0]; // only one sender for now
let fee = 0;
const counterpartyAmounts = new Map<string, number>();
parsedOutScripts.forEach((parsedScript, index) => {
const content = parsedScript?.file?.content;
if (!content) return;
const inscriptionData = Utils.toUTF8(content);
if (!inscriptionData) return;
let inscriptionJson: MneeInscription;
try {
inscriptionJson = JSON.parse(inscriptionData);
} catch (err) {
console.error('Failed to parse inscription JSON:', err);
return;
}
if (inscriptionJson.p !== 'bsv-20' || inscriptionJson.id !== config.tokenId) return;
const inscriptionAmt = parseInt(inscriptionJson.amt, 10);
if (Number.isNaN(inscriptionAmt)) return;
if (feeAddressIndex === index && sender === address) {
fee += inscriptionAmt;
return;
}
const outAddr = mneeAddresses[index];
const prevAmt = counterpartyAmounts.get(outAddr) || 0;
counterpartyAmounts.set(outAddr, prevAmt + inscriptionAmt);
});
const amountSentToAddress = counterpartyAmounts.get(address) || 0;
if (txType === 'send') {
const senderAmt = counterpartyAmounts.get(sender) || 0;
counterpartyAmounts.set(sender, senderAmt - amountSentToAddress);
}
let counterparties: { address: string; amount: number }[] = [];
if (txType === 'receive') {
counterparties = [{ address: sender, amount: amountSentToAddress }];
} else {
counterparties = Array.from(counterpartyAmounts.entries())
.map(([addr, amt]) => ({ address: addr, amount: amt }))
.filter((cp) => cp.address !== address && cp.address !== config.feeAddress && cp.amount > 0);
}
const totalCounterpartyAmount = counterparties.reduce((sum, cp) => sum + cp.amount, 0);
return {
txid: sync.txid,
height: sync.height,
type: txType,
status: txStatus,
amount: totalCounterpartyAmount,
fee,
score: sync.score,
counterparties,
};
};