622 lines
22 KiB
TypeScript
622 lines
22 KiB
TypeScript
import { binToHex, hexToBin, sha256 } from "@bitauth/libauth";
|
|
import { compileCashAssemblyString, type Engine } from "@xo-cash/engine";
|
|
import type { ScriptHashData, UnspentOutputData } from "@xo-cash/state";
|
|
import type {
|
|
XOInvitation,
|
|
XOInvitationCommit,
|
|
XOInvitationInput,
|
|
XOInvitationOutput,
|
|
XOInvitationVariableValue,
|
|
XOTemplate,
|
|
} from "@xo-cash/types";
|
|
import type { Invitation } from "./invitation.js";
|
|
|
|
export type WalletHistorySource = "invitation" | "utxo";
|
|
|
|
export type WalletHistoryInput = {
|
|
id: string;
|
|
commitIdentifier?: string;
|
|
inputIdentifier?: string;
|
|
role?: string;
|
|
description: string;
|
|
valueSatoshis?: bigint;
|
|
outpoint: {
|
|
txid: string;
|
|
index: number;
|
|
};
|
|
scriptHash?: string;
|
|
};
|
|
|
|
export type WalletHistoryOutput = {
|
|
id: string;
|
|
commitIdentifier?: string;
|
|
outputIdentifier?: string;
|
|
role?: string;
|
|
description: string;
|
|
valueSatoshis?: bigint;
|
|
outpoint?: {
|
|
txid: string;
|
|
index: number;
|
|
};
|
|
lockingBytecode?: string;
|
|
scriptHash?: string;
|
|
reserved?: boolean;
|
|
};
|
|
|
|
export type WalletHistoryItem = {
|
|
id: string;
|
|
source: WalletHistorySource;
|
|
invitationIdentifier?: string;
|
|
createdAtTimestamp?: number;
|
|
templateIdentifier: string;
|
|
template: string;
|
|
action?: string;
|
|
roles: string[];
|
|
description: string;
|
|
valueSatoshis: bigint;
|
|
inputs: WalletHistoryInput[];
|
|
outputs: WalletHistoryOutput[];
|
|
};
|
|
|
|
export type HistoryItem = WalletHistoryItem;
|
|
|
|
interface InvitationContext {
|
|
invitation: Invitation;
|
|
template: XOTemplate | null;
|
|
variables: Record<string, XOInvitationVariableValue>;
|
|
}
|
|
|
|
interface UtxoContext {
|
|
utxo: UnspentOutputData;
|
|
scriptHashData?: ScriptHashData;
|
|
template: XOTemplate | null;
|
|
}
|
|
|
|
interface WalletMetadataIndex {
|
|
scriptHashDataByScriptHash: Map<string, ScriptHashData>;
|
|
}
|
|
|
|
/*
|
|
* This needs a thorough and significant rewrite and design.
|
|
* I've tried to fundamental approaches so far:
|
|
* - UTXO first
|
|
* - Invitation first
|
|
*
|
|
* The issue is that neither of these end up being simple or effective
|
|
* UTXO first makes tracking utxos across invitations extremely difficult. So if you receive a UTXO from an invitation and then spend it on another, you wont even see that old invitation.
|
|
* Invitation first makes fitting UTXOs that dont have an invitation (say if someone sent directly to your address) extremely difficult. You end up having to run a UTXO first pass anyway, and then end up with conflicts around resolved roles.
|
|
* Inferring roles is also extremely difficult. We cant just say "does this have an output for our P2PKH receiving roll? it does? Ok, we are a receiver" because this would match `true` because of our change outputs.
|
|
* If anyone has any idea of how to address this without tying knots of spaghetti, please let me know.
|
|
* This has been rewritten multiple times to try and simplify it, but its still extremely hard to follow and understand, while not even providing information that we want.
|
|
*/
|
|
|
|
export class HistoryService {
|
|
constructor(
|
|
private engine: Engine,
|
|
private invitations: Invitation[],
|
|
) {}
|
|
|
|
|
|
/**
|
|
* I Might swap this over to invitation based history before the event to make it a bit more evident... Really not happy with the UTXO for demo purposes
|
|
* But for the actual usage, UTXO is easier to follow - just not good for demo
|
|
* Long term, this is intended to be in the Engine, so we will just be a consumer of history state.
|
|
*/
|
|
async getHistory(): Promise<WalletHistoryItem[]> {
|
|
const allUtxos = await this.engine.listUnspentOutputsData();
|
|
const metadataIndex = await this.buildWalletMetadataIndex(allUtxos);
|
|
const invitationContexts = await this.buildInvitationContextIndex();
|
|
const utxoContexts = await Promise.all(
|
|
allUtxos.map((utxo) => this.buildUtxoContext(utxo, metadataIndex)),
|
|
);
|
|
|
|
const reservedUtxosByInvitation = new Map<string, UtxoContext[]>();
|
|
const standaloneUtxos: UtxoContext[] = [];
|
|
|
|
for (const context of utxoContexts) {
|
|
const invitationIdentifier = context.utxo.reservedBy;
|
|
if (invitationIdentifier && invitationContexts.has(invitationIdentifier)) {
|
|
const group = reservedUtxosByInvitation.get(invitationIdentifier) ?? [];
|
|
group.push(context);
|
|
reservedUtxosByInvitation.set(invitationIdentifier, group);
|
|
continue;
|
|
}
|
|
|
|
standaloneUtxos.push(context);
|
|
}
|
|
|
|
const invitationItems = [...reservedUtxosByInvitation.entries()].map(
|
|
([invitationIdentifier, reservedContexts]) =>
|
|
this.projectInvitationHistory(
|
|
invitationContexts.get(invitationIdentifier)!,
|
|
reservedContexts,
|
|
),
|
|
);
|
|
const standaloneItems = standaloneUtxos.map((context) =>
|
|
this.projectStandaloneUtxo(context),
|
|
);
|
|
|
|
return [...standaloneItems, ...invitationItems].sort((a, b) => {
|
|
if (a.source !== b.source) return a.source === "utxo" ? -1 : 1;
|
|
return (b.createdAtTimestamp ?? 0) - (a.createdAtTimestamp ?? 0);
|
|
});
|
|
}
|
|
|
|
private async buildInvitationContextIndex(): Promise<Map<string, InvitationContext>> {
|
|
const contexts = new Map<string, InvitationContext>();
|
|
|
|
for (const invitation of this.invitations) {
|
|
const template =
|
|
(await this.engine.getTemplate(invitation.data.templateIdentifier)) ?? null;
|
|
contexts.set(invitation.data.invitationIdentifier, {
|
|
invitation,
|
|
template,
|
|
variables: this.extractInvitationVariables(invitation.data),
|
|
});
|
|
}
|
|
|
|
return contexts;
|
|
}
|
|
|
|
private async buildWalletMetadataIndex(
|
|
allUtxos: UnspentOutputData[],
|
|
): Promise<WalletMetadataIndex> {
|
|
const scriptHashDataByScriptHash = new Map<string, ScriptHashData>();
|
|
const templateIdentifiers = new Set<string>();
|
|
|
|
for (const utxo of allUtxos) {
|
|
templateIdentifiers.add(utxo.templateIdentifier);
|
|
}
|
|
for (const invitation of this.invitations) {
|
|
templateIdentifiers.add(invitation.data.templateIdentifier);
|
|
}
|
|
|
|
for (const templateIdentifier of templateIdentifiers) {
|
|
const scriptHashDataList = await this.engine.listScriptHashesForTemplate(templateIdentifier);
|
|
for (const scriptHashData of scriptHashDataList) {
|
|
scriptHashDataByScriptHash.set(scriptHashData.scriptHash, scriptHashData);
|
|
}
|
|
}
|
|
|
|
return { scriptHashDataByScriptHash };
|
|
}
|
|
|
|
private async buildUtxoContext(
|
|
utxo: UnspentOutputData,
|
|
metadataIndex: WalletMetadataIndex,
|
|
): Promise<UtxoContext> {
|
|
const scriptHashData = metadataIndex.scriptHashDataByScriptHash.get(utxo.scriptHash);
|
|
const templateIdentifier = scriptHashData?.templateIdentifier ?? utxo.templateIdentifier;
|
|
const template = (await this.engine.getTemplate(templateIdentifier)) ?? null;
|
|
|
|
return {
|
|
utxo,
|
|
scriptHashData,
|
|
template,
|
|
};
|
|
}
|
|
|
|
private projectInvitationHistory(
|
|
context: InvitationContext,
|
|
reservedContexts: UtxoContext[],
|
|
): WalletHistoryItem {
|
|
const invitation = context.invitation.data;
|
|
const entityRoles = this.deriveInvitationEntityRoles(context);
|
|
const inputs = this.projectInvitationInputs(context, reservedContexts, entityRoles);
|
|
const inputUtxoIds = this.listInvitationInputUtxoIds(context, reservedContexts);
|
|
const outputs = this.projectInvitationOutputs(
|
|
context,
|
|
reservedContexts,
|
|
entityRoles,
|
|
inputUtxoIds,
|
|
);
|
|
const roles = this.deriveRoles(inputs, outputs);
|
|
const valueSatoshis = this.calculateValueSatoshis(inputs, outputs);
|
|
|
|
return {
|
|
id: `inv-${invitation.invitationIdentifier}`,
|
|
source: "invitation",
|
|
invitationIdentifier: invitation.invitationIdentifier,
|
|
createdAtTimestamp: invitation.createdAtTimestamp,
|
|
templateIdentifier: invitation.templateIdentifier,
|
|
template: context.template?.name ?? "UnknownTemplate",
|
|
action: invitation.actionIdentifier,
|
|
roles,
|
|
description: this.describeInvitation(context, roles[0]),
|
|
valueSatoshis,
|
|
inputs,
|
|
outputs,
|
|
};
|
|
}
|
|
|
|
private projectInvitationInputs(
|
|
context: InvitationContext,
|
|
reservedContexts: UtxoContext[],
|
|
entityRoles: Map<string, string[]>,
|
|
): WalletHistoryInput[] {
|
|
const invitation = context.invitation.data;
|
|
const inputs: WalletHistoryInput[] = [];
|
|
const reservedByOutpoint = new Map(
|
|
reservedContexts.map((context) => [
|
|
this.getOutpointKey(
|
|
context.utxo.outpointTransactionHash,
|
|
context.utxo.outpointIndex,
|
|
),
|
|
context,
|
|
]),
|
|
);
|
|
|
|
for (const commit of invitation.commits) {
|
|
for (const [index, input] of (commit.data.inputs ?? []).entries()) {
|
|
const txid = this.getInputTxid(input);
|
|
const outpointIndex = input.outpointIndex;
|
|
if (txid === undefined || outpointIndex === undefined) continue;
|
|
|
|
const utxoContext = reservedByOutpoint.get(this.getOutpointKey(txid, outpointIndex));
|
|
// TODO: Remove this reservation-based filter once Engine/library cleanup releases stale invitation reservations internally.
|
|
if (!utxoContext) continue;
|
|
|
|
const inputIdentifier = input.inputIdentifier;
|
|
const role =
|
|
input.roleIdentifier ??
|
|
this.getFirstEntityRole(entityRoles, commit.entityIdentifier) ??
|
|
utxoContext.scriptHashData?.roleIdentifier;
|
|
|
|
inputs.push({
|
|
id: `input-${invitation.invitationIdentifier}-${commit.commitIdentifier}-${index}`,
|
|
commitIdentifier: commit.commitIdentifier,
|
|
inputIdentifier,
|
|
role,
|
|
description: this.describeInput(inputIdentifier, context),
|
|
valueSatoshis: -BigInt(utxoContext.utxo.valueSatoshis),
|
|
outpoint: { txid, index: outpointIndex },
|
|
scriptHash: utxoContext.utxo.scriptHash,
|
|
});
|
|
}
|
|
}
|
|
|
|
return inputs;
|
|
}
|
|
|
|
private projectInvitationOutputs(
|
|
context: InvitationContext,
|
|
reservedContexts: UtxoContext[],
|
|
entityRoles: Map<string, string[]>,
|
|
inputUtxoIds: Set<string>,
|
|
): WalletHistoryOutput[] {
|
|
const invitation = context.invitation.data;
|
|
const outputs: WalletHistoryOutput[] = [];
|
|
const usedUtxoIds = new Set<string>();
|
|
|
|
for (const commit of invitation.commits) {
|
|
for (const [index, output] of (commit.data.outputs ?? []).entries()) {
|
|
const matchingContext = this.findReservedOutputContext(
|
|
output,
|
|
reservedContexts,
|
|
usedUtxoIds,
|
|
);
|
|
// UTXO-first: committed outputs only matter here if they resolve to a wallet UTXO currently reserved by this invitation.
|
|
if (!matchingContext) continue;
|
|
|
|
const lockingBytecode = this.getOutputLockingBytecodeHex(output) ?? matchingContext.scriptHashData?.lockingBytecode;
|
|
const outputIdentifier = output.outputIdentifier ?? matchingContext.scriptHashData?.outputIdentifier ?? matchingContext.utxo.outputIdentifier;
|
|
const role =
|
|
output.roleIdentifier ??
|
|
this.getFirstEntityRole(entityRoles, commit.entityIdentifier) ??
|
|
matchingContext.scriptHashData?.roleIdentifier;
|
|
const valueSatoshis = output.valueSatoshis !== undefined
|
|
? BigInt(output.valueSatoshis)
|
|
: BigInt(matchingContext.utxo.valueSatoshis);
|
|
|
|
usedUtxoIds.add(this.getUtxoId(matchingContext.utxo));
|
|
|
|
outputs.push({
|
|
id: `output-${invitation.invitationIdentifier}-${commit.commitIdentifier}-${index}`,
|
|
commitIdentifier: commit.commitIdentifier,
|
|
outputIdentifier,
|
|
role,
|
|
description: this.describeOutput(outputIdentifier, context),
|
|
valueSatoshis,
|
|
outpoint: {
|
|
txid: matchingContext.utxo.outpointTransactionHash,
|
|
index: matchingContext.utxo.outpointIndex,
|
|
},
|
|
lockingBytecode,
|
|
scriptHash: matchingContext.utxo.scriptHash,
|
|
reserved: true,
|
|
});
|
|
}
|
|
}
|
|
|
|
for (const reservedContext of reservedContexts) {
|
|
if (usedUtxoIds.has(this.getUtxoId(reservedContext.utxo))) continue;
|
|
if (inputUtxoIds.has(this.getUtxoId(reservedContext.utxo))) continue;
|
|
outputs.push(this.projectUtxoOutput(reservedContext));
|
|
}
|
|
|
|
return outputs;
|
|
}
|
|
|
|
private listInvitationInputUtxoIds(
|
|
context: InvitationContext,
|
|
reservedContexts: UtxoContext[],
|
|
): Set<string> {
|
|
const invitationInputUtxoIds = new Set<string>();
|
|
const reservedByOutpoint = new Map(
|
|
reservedContexts.map((context) => [
|
|
this.getOutpointKey(
|
|
context.utxo.outpointTransactionHash,
|
|
context.utxo.outpointIndex,
|
|
),
|
|
context,
|
|
]),
|
|
);
|
|
|
|
for (const commit of context.invitation.data.commits) {
|
|
for (const input of commit.data.inputs ?? []) {
|
|
const txid = this.getInputTxid(input);
|
|
const outpointIndex = input.outpointIndex;
|
|
if (txid === undefined || outpointIndex === undefined) continue;
|
|
|
|
const utxoContext = reservedByOutpoint.get(this.getOutpointKey(txid, outpointIndex));
|
|
if (utxoContext) invitationInputUtxoIds.add(this.getUtxoId(utxoContext.utxo));
|
|
}
|
|
}
|
|
|
|
return invitationInputUtxoIds;
|
|
}
|
|
|
|
private findReservedOutputContext(
|
|
output: XOInvitationOutput,
|
|
reservedContexts: UtxoContext[],
|
|
usedUtxoIds: Set<string>,
|
|
): UtxoContext | undefined {
|
|
const lockingBytecode = this.getOutputLockingBytecodeHex(output);
|
|
const scriptHash = lockingBytecode
|
|
? this.lockingBytecodeToScriptHash(lockingBytecode)
|
|
: undefined;
|
|
|
|
return reservedContexts.find((context) => {
|
|
if (usedUtxoIds.has(this.getUtxoId(context.utxo))) return false;
|
|
if (scriptHash && context.utxo.scriptHash === scriptHash) return true;
|
|
if (lockingBytecode && context.scriptHashData?.lockingBytecode === lockingBytecode) return true;
|
|
if (output.outputIdentifier && context.utxo.outputIdentifier === output.outputIdentifier) return true;
|
|
return false;
|
|
});
|
|
}
|
|
|
|
private projectStandaloneUtxo(context: UtxoContext): WalletHistoryItem {
|
|
const output = this.projectUtxoOutput(context);
|
|
const templateIdentifier = context.scriptHashData?.templateIdentifier ?? context.utxo.templateIdentifier;
|
|
const role = output.role;
|
|
|
|
return {
|
|
id: `utxo-${context.utxo.outpointTransactionHash}:${context.utxo.outpointIndex}`,
|
|
source: "utxo",
|
|
templateIdentifier,
|
|
template: context.template?.name ?? "UnknownTemplate",
|
|
roles: role ? [role] : ["unknown"],
|
|
description: output.description,
|
|
valueSatoshis: output.valueSatoshis ?? 0n,
|
|
inputs: [],
|
|
outputs: [output],
|
|
};
|
|
}
|
|
|
|
private projectUtxoOutput(context: UtxoContext): WalletHistoryOutput {
|
|
const outputIdentifier = context.scriptHashData?.outputIdentifier ?? context.utxo.outputIdentifier;
|
|
const role = context.scriptHashData?.roleIdentifier;
|
|
|
|
return {
|
|
id: this.getUtxoId(context.utxo),
|
|
outputIdentifier,
|
|
role,
|
|
description: this.describeOutputFromTemplate(outputIdentifier, context.template, {}),
|
|
valueSatoshis: BigInt(context.utxo.valueSatoshis),
|
|
outpoint: {
|
|
txid: context.utxo.outpointTransactionHash,
|
|
index: context.utxo.outpointIndex,
|
|
},
|
|
lockingBytecode: context.scriptHashData?.lockingBytecode,
|
|
scriptHash: context.utxo.scriptHash,
|
|
reserved: context.utxo.reservedBy !== undefined,
|
|
};
|
|
}
|
|
|
|
private deriveInvitationEntityRoles(context: InvitationContext): Map<string, string[]> {
|
|
const invitation = context.invitation.data;
|
|
const rolesByEntity = new Map<string, Set<string>>();
|
|
const allEntities = new Set(invitation.commits.map((commit) => commit.entityIdentifier));
|
|
|
|
for (const entityIdentifier of allEntities) {
|
|
rolesByEntity.set(entityIdentifier, new Set());
|
|
}
|
|
|
|
for (const commit of invitation.commits) {
|
|
const roles = rolesByEntity.get(commit.entityIdentifier) ?? new Set<string>();
|
|
for (const input of commit.data.inputs ?? []) {
|
|
if (input.roleIdentifier) roles.add(input.roleIdentifier);
|
|
}
|
|
for (const output of commit.data.outputs ?? []) {
|
|
if (output.roleIdentifier) roles.add(output.roleIdentifier);
|
|
}
|
|
for (const variable of commit.data.variables ?? []) {
|
|
if (variable.roleIdentifier) roles.add(variable.roleIdentifier);
|
|
}
|
|
rolesByEntity.set(commit.entityIdentifier, roles);
|
|
}
|
|
|
|
const action = context.template?.actions?.[invitation.actionIdentifier];
|
|
const participantRoles = action?.requirements?.participants
|
|
?.map((participant) => participant.role)
|
|
.filter((role): role is string => typeof role === "string") ?? [];
|
|
const explicitlyFilledRoles = new Set<string>();
|
|
for (const roles of rolesByEntity.values()) {
|
|
for (const role of roles) explicitlyFilledRoles.add(role);
|
|
}
|
|
const unfilledParticipantRoles = participantRoles.filter(
|
|
(role) => !explicitlyFilledRoles.has(role),
|
|
);
|
|
const entitiesWithoutRoles = [...rolesByEntity.entries()]
|
|
.filter(([, roles]) => roles.size === 0)
|
|
.map(([entityIdentifier]) => entityIdentifier);
|
|
|
|
if (unfilledParticipantRoles.length === 1 && entitiesWithoutRoles.length >= 1) {
|
|
const inferredRole = unfilledParticipantRoles[0];
|
|
if (inferredRole !== undefined) {
|
|
for (const entityIdentifier of entitiesWithoutRoles) {
|
|
rolesByEntity.get(entityIdentifier)?.add(inferredRole);
|
|
}
|
|
}
|
|
}
|
|
|
|
return new Map(
|
|
[...rolesByEntity.entries()].map(([entityIdentifier, roles]) => [
|
|
entityIdentifier,
|
|
[...roles],
|
|
]),
|
|
);
|
|
}
|
|
|
|
private getFirstEntityRole(
|
|
entityRoles: Map<string, string[]>,
|
|
entityIdentifier: string,
|
|
): string | undefined {
|
|
return entityRoles.get(entityIdentifier)?.[0];
|
|
}
|
|
|
|
private deriveRoles(
|
|
inputs: WalletHistoryInput[],
|
|
outputs: WalletHistoryOutput[],
|
|
): string[] {
|
|
const roles = new Set<string>();
|
|
|
|
for (const input of inputs) {
|
|
if (input.role) roles.add(input.role);
|
|
}
|
|
for (const output of outputs) {
|
|
if (output.role) roles.add(output.role);
|
|
}
|
|
|
|
return roles.size > 0 ? [...roles] : ["unknown"];
|
|
}
|
|
|
|
private calculateValueSatoshis(
|
|
inputs: WalletHistoryInput[],
|
|
outputs: WalletHistoryOutput[],
|
|
): bigint {
|
|
const inputTotal = inputs.reduce((total, input) => total + (input.valueSatoshis ?? 0n), 0n);
|
|
const outputTotal = outputs.reduce((total, output) => total + (output.valueSatoshis ?? 0n), 0n);
|
|
return inputTotal + outputTotal;
|
|
}
|
|
|
|
private describeInvitation(context: InvitationContext, role?: string): string {
|
|
const invitation = context.invitation.data;
|
|
const template = context.template;
|
|
if (!template) return invitation.actionIdentifier;
|
|
|
|
const action = template.actions?.[invitation.actionIdentifier];
|
|
const transaction = action?.transaction
|
|
? template.transactions?.[action.transaction]
|
|
: undefined;
|
|
const roleData = role ? transaction?.roles?.[role] : undefined;
|
|
const descriptionTemplate =
|
|
roleData?.description ??
|
|
transaction?.description ??
|
|
roleData?.name ??
|
|
transaction?.name ??
|
|
action?.description ??
|
|
action?.name ??
|
|
invitation.actionIdentifier;
|
|
|
|
return this.compileDescription(descriptionTemplate, context.variables);
|
|
}
|
|
|
|
private describeInput(inputIdentifier: string | undefined, context: InvitationContext): string {
|
|
if (!inputIdentifier) return "Input";
|
|
const input = context.template?.inputs?.[inputIdentifier];
|
|
return this.compileDescription(input?.description ?? input?.name ?? inputIdentifier, context.variables);
|
|
}
|
|
|
|
private describeOutput(outputIdentifier: string | undefined, context: InvitationContext): string {
|
|
return this.describeOutputFromTemplate(outputIdentifier, context.template, context.variables);
|
|
}
|
|
|
|
private describeOutputFromTemplate(
|
|
outputIdentifier: string | undefined,
|
|
template: XOTemplate | null,
|
|
variables: Record<string, XOInvitationVariableValue>,
|
|
): string {
|
|
if (!outputIdentifier) return "Output";
|
|
const output = template?.outputs?.[outputIdentifier];
|
|
return this.compileDescription(output?.description ?? output?.name ?? outputIdentifier, variables);
|
|
}
|
|
|
|
private compileDescription(
|
|
description: string,
|
|
variables: Record<string, XOInvitationVariableValue>,
|
|
): string {
|
|
try {
|
|
return compileCashAssemblyString(description, variables);
|
|
} catch {
|
|
return this.interpolateSimpleCashAssemblyVariables(description, variables);
|
|
}
|
|
}
|
|
|
|
private extractInvitationVariables(
|
|
invitation: XOInvitation,
|
|
): Record<string, XOInvitationVariableValue> {
|
|
const committedVariables = invitation.commits.flatMap((c) => c.data.variables ?? []);
|
|
return committedVariables.reduce(
|
|
(acc, variable) => {
|
|
if (!variable.variableIdentifier) return acc;
|
|
acc[variable.variableIdentifier] = variable.value;
|
|
return acc;
|
|
},
|
|
{} as Record<string, XOInvitationVariableValue>,
|
|
);
|
|
}
|
|
|
|
private getInputTxid(input: XOInvitationInput): string | undefined {
|
|
if (!input.outpointTransactionHash) return undefined;
|
|
return input.outpointTransactionHash instanceof Uint8Array
|
|
? binToHex(input.outpointTransactionHash)
|
|
: String(input.outpointTransactionHash);
|
|
}
|
|
|
|
private getOutputLockingBytecodeHex(output: XOInvitationOutput): string | undefined {
|
|
if (output.lockingBytecode === undefined) return undefined;
|
|
return typeof output.lockingBytecode === "string"
|
|
? output.lockingBytecode
|
|
: binToHex(output.lockingBytecode);
|
|
}
|
|
|
|
private getOutpointKey(txid: string, index: number): string {
|
|
return `${txid}:${index}`;
|
|
}
|
|
|
|
private getUtxoId(utxo: UnspentOutputData): string {
|
|
return `utxo-${utxo.outpointTransactionHash}:${utxo.outpointIndex}`;
|
|
}
|
|
|
|
private lockingBytecodeToScriptHash(lockingBytecode: string): string {
|
|
const hash = sha256.hash(hexToBin(lockingBytecode));
|
|
return binToHex(hash.reverse());
|
|
}
|
|
|
|
private interpolateSimpleCashAssemblyVariables(
|
|
text: string,
|
|
variables: Record<string, XOInvitationVariableValue>,
|
|
): string {
|
|
return text.replace(
|
|
/\$\(<([^>]+)>\)/g,
|
|
(match, variableIdentifier: string) => {
|
|
if (!Object.prototype.hasOwnProperty.call(variables, variableIdentifier)) {
|
|
return match;
|
|
}
|
|
return String(variables[variableIdentifier]);
|
|
},
|
|
);
|
|
}
|
|
}
|