diff --git a/readme.md b/readme.md index f122c03..52545e4 100644 --- a/readme.md +++ b/readme.md @@ -3,6 +3,7 @@ ## Installation ### Full Installation + ```bash # Create a new directory since we are going to be pulling in engine too mkdir xo-terminal && cd xo-terminal @@ -131,27 +132,32 @@ These commands add `XO_CONFIG_DIR` to your shell config with a default of generated assignment, to use a different wallet-state directory. #### Install for bash + ```bash npm run autocomplete:install:bash ``` #### Install for zsh + ```bash npm run autocomplete:install:zsh ``` #### Install for fish + ```bash npm run autocomplete:install:fish ``` ### Run the CLI + ```bash # If globally installed (Not really usable if not globally installed) xo-cli ``` ### Run the TUI + ```bash # If globally installed xo-tui diff --git a/src/cli/README.md b/src/cli/README.md index c320455..81abd1e 100644 --- a/src/cli/README.md +++ b/src/cli/README.md @@ -11,8 +11,8 @@ There are two global commands after install: Wallet state lives under **`${XO_CONFIG_DIR:-~/.config/xo-cli}`**, so you can run commands from any directory. Set `XO_CONFIG_DIR` to use a different wallet-state root. -| Path | Purpose | -| -------------------------- | ----------------------------------------------------------------------- | +| Path | Purpose | +| --------------------------- | ----------------------------------------------------------------------- | | `$XO_CONFIG_DIR/mnemonics/` | Mnemonic files (`mnemonic-*`) | | `$XO_CONFIG_DIR/data/` | Engine DB (`xo-wallet.db`) and invitation storage (`xo-invitations.db`) | | `$XO_CONFIG_DIR/.wallet` | JSON settings (`default-mnemonic`, `currency`) | @@ -41,13 +41,13 @@ npx tsx src/index.ts # TUI ### Environment variables -| Variable | Default | -| ------------------------- | ----------------------------------------- | -| `XO_CONFIG_DIR` | `~/.config/xo-cli` | -| `SYNC_SERVER_URL` | `http://localhost:3000` | -| `DB_PATH` | `$XO_CONFIG_DIR/data` | -| `DB_FILENAME` | `xo-wallet.db` | -| `INVITATION_STORAGE_PATH` | `$XO_CONFIG_DIR/data/xo-invitations.db` | +| Variable | Default | +| ------------------------- | --------------------------------------- | +| `XO_CONFIG_DIR` | `~/.config/xo-cli` | +| `SYNC_SERVER_URL` | `http://localhost:3000` | +| `DB_PATH` | `$XO_CONFIG_DIR/data` | +| `DB_FILENAME` | `xo-wallet.db` | +| `INVITATION_STORAGE_PATH` | `$XO_CONFIG_DIR/data/xo-invitations.db` | Use an absolute path for a custom root. Setting `XO_CONFIG_DIR` does not copy state from the default directory. @@ -88,13 +88,13 @@ xo-cli resource list ## Global Options (`xo-cli`) -| Flag | Description | -| ------------------------------ | --------------------------------------------------- | -| `-m`, `--mnemonic-file ` | Mnemonic file (basename, cwd-relative, or absolute) | +| Flag | Description | +| ------------------------------ | ---------------------------------------------------- | +| `-m`, `--mnemonic-file ` | Mnemonic file (basename, cwd-relative, or absolute) | | `--currency ` | Fiat display currency (e.g. `USD`, `AUD`) | | `-o`, `--output ` | Output filename (used by `mnemonic create`/`import`) | -| `-v`, `--verbose` | Verbose output | -| `-h`, `--help` | Help | +| `-v`, `--verbose` | Verbose output | +| `-h`, `--help` | Help | Advanced: you can pass `--database-path`, `--database-filename`, and `--invitation-storage-path` to override the defaults under `$XO_CONFIG_DIR/data/` (see `src/cli/index.ts`). diff --git a/src/cli/autocomplete/completions.ts b/src/cli/autocomplete/completions.ts index f71aa96..27e593d 100644 --- a/src/cli/autocomplete/completions.ts +++ b/src/cli/autocomplete/completions.ts @@ -19,12 +19,7 @@ * xo-cli completions fish --install */ -import { - existsSync, - readFileSync, - appendFileSync, - mkdirSync, -} from "node:fs"; +import { existsSync, readFileSync, appendFileSync, mkdirSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { homedir } from "node:os"; @@ -216,13 +211,15 @@ const shellConfigs: Record< > = { bash: { configFile: join(homedir(), ".bashrc"), - configDirCommand: 'export XO_CONFIG_DIR="${XO_CONFIG_DIR:-$HOME/.config/xo-cli}"', + configDirCommand: + 'export XO_CONFIG_DIR="${XO_CONFIG_DIR:-$HOME/.config/xo-cli}"', configDirPattern: /^\s*(?:export\s+)?XO_CONFIG_DIR=/m, evalCommand: (binName) => `eval "$(${binName} completions bash)"`, }, zsh: { configFile: join(homedir(), ".zshrc"), - configDirCommand: 'export XO_CONFIG_DIR="${XO_CONFIG_DIR:-$HOME/.config/xo-cli}"', + configDirCommand: + 'export XO_CONFIG_DIR="${XO_CONFIG_DIR:-$HOME/.config/xo-cli}"', configDirPattern: /^\s*(?:export\s+)?XO_CONFIG_DIR=/m, evalCommand: (binName) => `eval "$(${binName} completions zsh)"`, }, diff --git a/src/cli/commands/invitation.ts b/src/cli/commands/invitation.ts index 48c9df1..8c21461 100644 --- a/src/cli/commands/invitation.ts +++ b/src/cli/commands/invitation.ts @@ -23,7 +23,10 @@ const DUST_THRESHOLD = 546n; /** * Serializes an invitation to pretty-printed JSON for file export. */ -const formatInvitationForFile = (invitation: XOInvitation, indent = 2): string => +const formatInvitationForFile = ( + invitation: XOInvitation, + indent = 2, +): string => JSON.stringify(JSON.parse(serializeInvitation(invitation)), null, indent); /** @@ -358,8 +361,7 @@ export const handleInvitationExportCommand = async ( } const invitation = deps.app.invitations.find( - (candidate) => - candidate.data.invitationIdentifier === invitationIdentifier, + (candidate) => candidate.data.invitationIdentifier === invitationIdentifier, ); if (!invitation) { @@ -499,7 +501,9 @@ export const handleInvitationCommand = async ( hasMissingRequirements(missingRequirements.templateRequirements) || missingRequirements.inputsMissingSignatures.length > 0; - deps.io.verbose(`Missing requirements: ${formatObject(missingRequirements)}`); + deps.io.verbose( + `Missing requirements: ${formatObject(missingRequirements)}`, + ); deps.io.verbose(`Has missing requirements: ${hasMissing}`); // If there are missing requirements, print them out @@ -693,7 +697,7 @@ export const handleInvitationCommand = async ( // Return the invitation identifier return { invitationIdentifier }; } - + case "broadcast": { // Get the invitation identifier from the arguments const invitationIdentifier = args[1]; @@ -940,7 +944,7 @@ export const handleInvitationCommand = async ( deps.io.verbose( `Invitation created: ${formatObject(invitationInstance.data)}`, ); - + // Return the invitation identifier return { invitationIdentifier: invitationInstance.data.invitationIdentifier, diff --git a/src/cli/commands/resource.ts b/src/cli/commands/resource.ts index 0058a1f..d6dfdbe 100644 --- a/src/cli/commands/resource.ts +++ b/src/cli/commands/resource.ts @@ -37,7 +37,9 @@ function formatResource( showReserved = false, ): string { // Format the template - const template = resource.template ? dim(`[${generateTemplateIdentifier(resource.template)}]`) : ""; + const template = resource.template + ? dim(`[${generateTemplateIdentifier(resource.template)}]`) + : ""; // Format the outpoint const outpoint = bold( @@ -51,7 +53,7 @@ function formatResource( const output = resource.outputIdentifier ? dim(resource.outputIdentifier) : ""; - + // Format the height const height = dim(`(height ${resource.minedAtHeight})`); @@ -233,7 +235,7 @@ export const handleResourceCommand = async ( deps.io.out( `Unreserved ${bold(`${txHash}:${vout}`)} (was reserved for ${target.reservedBy})`, ); - + // TODO: What do I want to return here? return {}; } diff --git a/src/cli/commands/settings.ts b/src/cli/commands/settings.ts index 0b4fbff..2ae0b12 100644 --- a/src/cli/commands/settings.ts +++ b/src/cli/commands/settings.ts @@ -83,7 +83,7 @@ export const handleSettingsCommand = async ( const value = key === "currency" ? settings.getCurrency() - : settings.getDefaultMnemonic() ?? ""; + : (settings.getDefaultMnemonic() ?? ""); deps.io.out(value); return { key, value }; } diff --git a/src/cli/commands/template.ts b/src/cli/commands/template.ts index 964662d..e57b61c 100644 --- a/src/cli/commands/template.ts +++ b/src/cli/commands/template.ts @@ -4,7 +4,10 @@ import { generateTemplateIdentifier } from "@xo-cash/engine"; import type { XOTemplate } from "@xo-cash/types"; import { bold, dim, formatObject } from "../utils.js"; -import { loadTemplateFromFile, TemplateLoadError } from "../../utils/load-template-from-file.js"; +import { + loadTemplateFromFile, + TemplateLoadError, +} from "../../utils/load-template-from-file.js"; import { resolveTemplateReferences } from "../../utils/templates.js"; import type { CommandDependencies, CommandIO } from "./types.js"; import { CommandError } from "./types.js"; diff --git a/src/cli/index.ts b/src/cli/index.ts index ce272ef..bef5b3f 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -181,16 +181,20 @@ async function main(): Promise { // Create an App instance io.verbose("Creating app instance..."); - const app = await AppService.create(mnemonic, { - syncServerUrl: options["syncServerUrl"] ?? "http://localhost:3000", - engineConfig: { - databasePath: options["databasePath"] ?? paths.dataDir, - databaseFilename: options["databaseFilename"] ?? "xo-wallet.db", + const app = await AppService.create( + mnemonic, + { + syncServerUrl: options["syncServerUrl"] ?? "http://localhost:3000", + engineConfig: { + databasePath: options["databasePath"] ?? paths.dataDir, + databaseFilename: options["databaseFilename"] ?? "xo-wallet.db", + }, + invitationStoragePath: + options["invitationStoragePath"] ?? + join(paths.dataDir, "xo-invitations.db"), }, - invitationStoragePath: - options["invitationStoragePath"] ?? - join(paths.dataDir, "xo-invitations.db"), - }, settings); + settings, + ); io.verbose("App instance created"); // Start the app diff --git a/src/services/app.ts b/src/services/app.ts index febfc1f..f175a1b 100644 --- a/src/services/app.ts +++ b/src/services/app.ts @@ -100,8 +100,12 @@ export class AppService extends EventEmitter { const templates = await engine.listImportedTemplates(); templates.forEach(async (template) => { - engine.updateUnspentOutputsForTemplate(generateTemplateIdentifier(template)); - engine.subscribeToScriptHashForTemplate(generateTemplateIdentifier(template)); + engine.updateUnspentOutputsForTemplate( + generateTemplateIdentifier(template), + ); + engine.subscribeToScriptHashForTemplate( + generateTemplateIdentifier(template), + ); }); }; @@ -127,7 +131,14 @@ export class AppService extends EventEmitter { }); const rates = await RatesService.create(settings); - return new AppService(engine, walletStorage, config, electrum, rates, settings); + return new AppService( + engine, + walletStorage, + config, + electrum, + rates, + settings, + ); } constructor( @@ -298,9 +309,9 @@ export class AppService extends EventEmitter { async start(): Promise { // Start rates in the background so BCH -> fiat conversions become reactive in the TUI. - this.rates.start().catch((err) => - console.error('Error starting rates service:', err), - ); + this.rates + .start() + .catch((err) => console.error("Error starting rates service:", err)); // Get the invitations db const invitationsDb = this.storage.child("invitations"); diff --git a/src/services/history.ts b/src/services/history.ts index 3cd09e4..10b3367 100644 --- a/src/services/history.ts +++ b/src/services/history.ts @@ -80,7 +80,7 @@ interface WalletMetadataIndex { * 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. @@ -95,7 +95,6 @@ export class HistoryService { 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 @@ -114,7 +113,10 @@ export class HistoryService { for (const context of utxoContexts) { const invitationIdentifier = context.utxo.reservedBy; - if (invitationIdentifier && invitationContexts.has(invitationIdentifier)) { + if ( + invitationIdentifier && + invitationContexts.has(invitationIdentifier) + ) { const group = reservedUtxosByInvitation.get(invitationIdentifier) ?? []; group.push(context); reservedUtxosByInvitation.set(invitationIdentifier, group); @@ -141,13 +143,15 @@ export class HistoryService { }); } - private async buildInvitationContextIndex(): Promise> { + private async buildInvitationContextIndex(): Promise< + Map + > { const contexts = new Map(); for (const invitation of this.invitations) { const templateIdentifier = invitation.data.templateIdentifier; const template = templateIdentifier - ? (await this.engine.getTemplate(templateIdentifier)) ?? null + ? ((await this.engine.getTemplate(templateIdentifier)) ?? null) : null; contexts.set(invitation.data.invitationIdentifier, { invitation, @@ -181,9 +185,13 @@ export class HistoryService { } for (const templateIdentifier of templateIdentifiers) { - const scriptHashDataList = await this.engine.listScriptHashesForTemplate(templateIdentifier); + const scriptHashDataList = + await this.engine.listScriptHashesForTemplate(templateIdentifier); for (const scriptHashData of scriptHashDataList) { - scriptHashDataByScriptHash.set(scriptHashData.scriptHash, scriptHashData); + scriptHashDataByScriptHash.set( + scriptHashData.scriptHash, + scriptHashData, + ); } } @@ -194,10 +202,12 @@ export class HistoryService { utxo: UnspentOutputData, metadataIndex: WalletMetadataIndex, ): Promise { - const scriptHashData = metadataIndex.scriptHashDataByScriptHash.get(utxo.scriptHash); + const scriptHashData = metadataIndex.scriptHashDataByScriptHash.get( + utxo.scriptHash, + ); const templateIdentifier = scriptHashData?.templateIdentifier; const template = templateIdentifier - ? (await this.engine.getTemplate(templateIdentifier)) ?? null + ? ((await this.engine.getTemplate(templateIdentifier)) ?? null) : null; return { @@ -213,8 +223,15 @@ export class HistoryService { ): 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 inputs = this.projectInvitationInputs( + context, + reservedContexts, + entityRoles, + ); + const inputUtxoIds = this.listInvitationInputUtxoIds( + context, + reservedContexts, + ); const outputs = this.projectInvitationOutputs( context, reservedContexts, @@ -263,7 +280,9 @@ export class HistoryService { const outpointIndex = input.outpointIndex; if (txid === undefined || outpointIndex === undefined) continue; - const utxoContext = reservedByOutpoint.get(this.getOutpointKey(txid, outpointIndex)); + 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; @@ -309,15 +328,20 @@ export class HistoryService { // 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; + const lockingBytecode = + this.getOutputLockingBytecodeHex(output) ?? + matchingContext.scriptHashData?.lockingBytecode; + const outputIdentifier = + output.outputIdentifier ?? + matchingContext.scriptHashData?.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); + const valueSatoshis = + output.valueSatoshis !== undefined + ? BigInt(output.valueSatoshis) + : BigInt(matchingContext.utxo.valueSatoshis); usedUtxoIds.add(this.getUtxoId(matchingContext.utxo)); @@ -369,8 +393,11 @@ export class HistoryService { 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)); + const utxoContext = reservedByOutpoint.get( + this.getOutpointKey(txid, outpointIndex), + ); + if (utxoContext) + invitationInputUtxoIds.add(this.getUtxoId(utxoContext.utxo)); } } @@ -390,9 +417,17 @@ export class HistoryService { 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 ( + lockingBytecode && + context.scriptHashData?.lockingBytecode === lockingBytecode + ) + return true; - if (output.outputIdentifier && context.scriptHashData?.outputIdentifier === output.outputIdentifier) return true; + if ( + output.outputIdentifier && + context.scriptHashData?.outputIdentifier === output.outputIdentifier + ) + return true; return false; }); } @@ -423,7 +458,11 @@ export class HistoryService { id: this.getUtxoId(context.utxo), outputIdentifier, role, - description: this.describeOutputFromTemplate(outputIdentifier, context.template, {}), + description: this.describeOutputFromTemplate( + outputIdentifier, + context.template, + {}, + ), valueSatoshis: BigInt(context.utxo.valueSatoshis), outpoint: { txid: context.utxo.outpointTransactionHash, @@ -435,17 +474,22 @@ export class HistoryService { }; } - private deriveInvitationEntityRoles(context: InvitationContext): Map { + private deriveInvitationEntityRoles( + context: InvitationContext, + ): Map { const invitation = context.invitation.data; const rolesByEntity = new Map>(); - const allEntities = new Set(invitation.commits.map((commit) => commit.entityIdentifier)); + 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(); + const roles = + rolesByEntity.get(commit.entityIdentifier) ?? new Set(); for (const input of commit.data.inputs ?? []) { if (input.roleIdentifier) roles.add(input.roleIdentifier); } @@ -459,9 +503,10 @@ export class HistoryService { } 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 participantRoles = + action?.requirements?.participants + ?.map((participant) => participant.role) + .filter((role): role is string => typeof role === "string") ?? []; const explicitlyFilledRoles = new Set(); for (const roles of rolesByEntity.values()) { for (const role of roles) explicitlyFilledRoles.add(role); @@ -473,7 +518,10 @@ export class HistoryService { .filter(([, roles]) => roles.size === 0) .map(([entityIdentifier]) => entityIdentifier); - if (unfilledParticipantRoles.length === 1 && entitiesWithoutRoles.length >= 1) { + if ( + unfilledParticipantRoles.length === 1 && + entitiesWithoutRoles.length >= 1 + ) { const inferredRole = unfilledParticipantRoles[0]; if (inferredRole !== undefined) { for (const entityIdentifier of entitiesWithoutRoles) { @@ -517,12 +565,21 @@ export class HistoryService { 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); + 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 { + private describeInvitation( + context: InvitationContext, + role?: string, + ): string { const invitation = context.invitation.data; const template = context.template; if (!template) return invitation.actionIdentifier; @@ -544,14 +601,27 @@ export class HistoryService { return this.compileDescription(descriptionTemplate, context.variables); } - private describeInput(inputIdentifier: string | undefined, context: InvitationContext): string { + 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); + 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 describeOutput( + outputIdentifier: string | undefined, + context: InvitationContext, + ): string { + return this.describeOutputFromTemplate( + outputIdentifier, + context.template, + context.variables, + ); } private describeOutputFromTemplate( @@ -561,7 +631,10 @@ export class HistoryService { ): string { if (!outputIdentifier) return "Output"; const output = template?.outputs?.[outputIdentifier]; - return this.compileDescription(output?.description ?? output?.name ?? outputIdentifier, variables); + return this.compileDescription( + output?.description ?? output?.name ?? outputIdentifier, + variables, + ); } private compileDescription( @@ -569,16 +642,25 @@ export class HistoryService { variables: Record, ): string { try { - return compileCashAssemblyString({ cashAssemblyText: description, variables, evaluationDecodeMode: 'utf8' }); + return compileCashAssemblyString({ + cashAssemblyText: description, + variables, + evaluationDecodeMode: "utf8", + }); } catch { - return this.interpolateSimpleCashAssemblyVariables(description, variables); + return this.interpolateSimpleCashAssemblyVariables( + description, + variables, + ); } } private extractInvitationVariables( invitation: XOInvitation, ): Record { - const committedVariables = invitation.commits.flatMap((c) => c.data.variables ?? []); + const committedVariables = invitation.commits.flatMap( + (c) => c.data.variables ?? [], + ); return committedVariables.reduce( (acc, variable) => { if (!variable.variableIdentifier) return acc; @@ -596,15 +678,21 @@ export class HistoryService { : String(input.outpointTransactionHash); } - private getOutputLockingBytecodeHex(output: XOInvitationOutput): string | undefined { + private getOutputLockingBytecodeHex( + output: XOInvitationOutput, + ): string | undefined { if (output.lockingBytecode === undefined) return undefined; return typeof output.lockingBytecode === "string" ? output.lockingBytecode : binToHex(output.lockingBytecode); } - private async getScriptHashData(scriptHash: string): Promise { - return (this.engine as unknown as { state: State }).state.getScriptHashData(scriptHash); + private async getScriptHashData( + scriptHash: string, + ): Promise { + return (this.engine as unknown as { state: State }).state.getScriptHashData( + scriptHash, + ); } private getOutpointKey(txid: string, index: number): string { @@ -627,7 +715,9 @@ export class HistoryService { return text.replace( /\$\(<([^>]+)>\)/g, (match, variableIdentifier: string) => { - if (!Object.prototype.hasOwnProperty.call(variables, variableIdentifier)) { + if ( + !Object.prototype.hasOwnProperty.call(variables, variableIdentifier) + ) { return match; } return String(variables[variableIdentifier]); diff --git a/src/services/invitation.ts b/src/services/invitation.ts index b122ad9..73c22c3 100644 --- a/src/services/invitation.ts +++ b/src/services/invitation.ts @@ -3,7 +3,13 @@ import type { Engine, GetSpendableResourcesParameters, } from "@xo-cash/engine"; -import { generateTemplateIdentifier, hasInvitationExpired, mergeInvitationCommits, serializeInvitation, deserializeInvitation } from "@xo-cash/engine"; +import { + generateTemplateIdentifier, + hasInvitationExpired, + mergeInvitationCommits, + serializeInvitation, + deserializeInvitation, +} from "@xo-cash/engine"; import type { XOInvitation, XOInvitationCommit, @@ -92,7 +98,9 @@ export class Invitation extends EventEmitter { } // engine invitation (I have no idea if this is required) - const engineInvitation = await dependencies.engine.importInvitation(serializeInvitation(invitation)); + const engineInvitation = await dependencies.engine.importInvitation( + serializeInvitation(invitation), + ); // Create the invitation const invitationInstance = new Invitation(engineInvitation, dependencies); @@ -287,7 +295,9 @@ export class Invitation extends EventEmitter { return payload; } - private unwrapLegacyInvitationUpdatedPayload(payload: unknown): unknown | null { + private unwrapLegacyInvitationUpdatedPayload( + payload: unknown, + ): unknown | null { if ( payload && typeof payload === "object" && @@ -308,7 +318,10 @@ export class Invitation extends EventEmitter { invitation: XOInvitation = this.data, ): Promise { this.syncServer.publishInvitation(invitation).catch((error) => { - this.emit("error", error instanceof Error ? error : new Error(String(error))); + this.emit( + "error", + error instanceof Error ? error : new Error(String(error)), + ); }); } @@ -362,7 +375,9 @@ export class Invitation extends EventEmitter { private async computeStatusInternal(): Promise { let missingReqs; try { - const missingRequirements = await this.engine.listMissingRequirements(this.data.invitationIdentifier); + const missingRequirements = await this.engine.listMissingRequirements( + this.data.invitationIdentifier, + ); missingReqs = missingRequirements.templateRequirements; } catch { return "unknown"; @@ -454,13 +469,18 @@ export class Invitation extends EventEmitter { * Update the status of the invitation and emit the new single-word status. */ private async updateStatus(): Promise { - this.computeStatus().then(status => { - this.status = status; - this.emit("invitation-status-changed", status); - }).catch((error) => { - this.status = `error (${error instanceof Error ? error.message : String(error)})`; - this.emit("error", error instanceof Error ? error : new Error(String(error))); - }); + this.computeStatus() + .then((status) => { + this.status = status; + this.emit("invitation-status-changed", status); + }) + .catch((error) => { + this.status = `error (${error instanceof Error ? error.message : String(error)})`; + this.emit( + "error", + error instanceof Error ? error : new Error(String(error)), + ); + }); } /** @@ -499,7 +519,9 @@ export class Invitation extends EventEmitter { */ async sign(): Promise { // Sign the invitation - const signedInvitation = await this.engine.signInvitation(this.data.invitationIdentifier); + const signedInvitation = await this.engine.signInvitation( + this.data.invitationIdentifier, + ); // Publish the signed invitation to the sync server this.publishInvitation(signedInvitation); @@ -518,9 +540,12 @@ export class Invitation extends EventEmitter { * @returns The transaction hash returned by the network after broadcast. */ async broadcast(): Promise { - const txHash = await this.engine.executeAction(this.data.invitationIdentifier, { - broadcastTransaction: true, - }); + const txHash = await this.engine.executeAction( + this.data.invitationIdentifier, + { + broadcastTransaction: true, + }, + ); await this.updateStatus(); @@ -538,7 +563,10 @@ export class Invitation extends EventEmitter { await this.ensureAccepted(); // Append the commit to the invitation - this.data = await this.engine.appendInvitation(this.data.invitationIdentifier, data); + this.data = await this.engine.appendInvitation( + this.data.invitationIdentifier, + data, + ); // Sync the invitation to the sync server await this.publishInvitation(this.data); @@ -617,8 +645,8 @@ export class Invitation extends EventEmitter { const templates = await this.engine.listImportedTemplates(); // For each template, we need to create a 2d array of all the outputs - const outputs = templates.map(template => { - return Object.keys(template.outputs).map(output => { + const outputs = templates.map((template) => { + return Object.keys(template.outputs).map((output) => { const templateIdentifier = generateTemplateIdentifier(template); return { @@ -629,14 +657,18 @@ export class Invitation extends EventEmitter { }); // then, for each output, we need to get the spendable resources - const spendableResources = await Promise.all(outputs.flat().map(output => { - return this.engine.getSpendableResources(this.data, { - templateIdentifier: output.templateIdentifier, - outputIdentifier: output.outputIdentifier, - }); - })); + const spendableResources = await Promise.all( + outputs.flat().map((output) => { + return this.engine.getSpendableResources(this.data, { + templateIdentifier: output.templateIdentifier, + outputIdentifier: output.outputIdentifier, + }); + }), + ); - const unspentOutputs = spendableResources.flatMap(resource => resource.unspentOutputs); + const unspentOutputs = spendableResources.flatMap( + (resource) => resource.unspentOutputs, + ); // Update the status of the invitation await this.updateStatus(); @@ -738,9 +770,11 @@ export class Invitation extends EventEmitter { ); // Compile the CashAssembly expression to get the value satoshis (It handles the variable replacement for us) - const valueSatoshis = compileCashAssemblyString( - { cashAssemblyText: String(valueSatoshisExpression), variables: formattedVariables, evaluationDecodeMode: 'bigint' }, - ); + const valueSatoshis = compileCashAssemblyString({ + cashAssemblyText: String(valueSatoshisExpression), + variables: formattedVariables, + evaluationDecodeMode: "bigint", + }); // Return the value satoshis as a bigint // TODO: Check this of a vulnerability or crash - I assume there might be one if someone made the `valueSatoshis` a malicious expression @@ -796,7 +830,7 @@ export class Invitation extends EventEmitter { for (const output of outputs) { if (typeof output === "string") { const sats = await this.getSatsOut(output); - totalSats += sats + totalSats += sats; } else { const sats = await this.getSatsOut(output.output); totalSats += sats; diff --git a/src/services/rates.ts b/src/services/rates.ts index a03e6ca..fa35a7b 100644 --- a/src/services/rates.ts +++ b/src/services/rates.ts @@ -1,16 +1,14 @@ -import { OracleClient } from '@generalprotocols/oracle-client'; -import { EventEmitter } from '../utils/event-emitter.js'; -import { - type RatesEventMap, -} from '../utils/rates/base-rates.js'; -import { RatesOracle } from '../utils/rates/rates-oracles.js'; -import { SettingsService } from './settings.js'; +import { OracleClient } from "@generalprotocols/oracle-client"; +import { EventEmitter } from "../utils/event-emitter.js"; +import { type RatesEventMap } from "../utils/rates/base-rates.js"; +import { RatesOracle } from "../utils/rates/rates-oracles.js"; +import { SettingsService } from "./settings.js"; /** * Event map emitted by {@link RatesService}. */ export type RatesServiceEventMap = { - 'rate-updated': { + "rate-updated": { numeratorUnitCode: string; denominatorUnitCode: string; price: number; @@ -39,8 +37,8 @@ export interface RatesAdapter { listPairs(): Promise>; formatCurrency(amount: number, targetCurrency: string): string; on( - type: 'rateUpdated', - listener: (detail: RatesEventMap['rateUpdated']) => void, + type: "rateUpdated", + listener: (detail: RatesEventMap["rateUpdated"]) => void, ): () => void; } @@ -96,7 +94,7 @@ export class RatesService extends EventEmitter { } this.started = true; - this.unsubscribeFromAdapter = this.adapter.on('rateUpdated', (event) => { + this.unsubscribeFromAdapter = this.adapter.on("rateUpdated", (event) => { this.handleRateUpdated(event); }); @@ -145,9 +143,9 @@ export class RatesService extends EventEmitter { */ public convertBchToFiat( satoshis: bigint, - targetCurrency: string = 'USD', + targetCurrency: string = "USD", ): number | null { - const rate = this.getRate(targetCurrency, 'BCH'); + const rate = this.getRate(targetCurrency, "BCH"); if (rate === null) { return null; } @@ -161,7 +159,7 @@ export class RatesService extends EventEmitter { */ public formatBchToFiat( satoshis: bigint, - targetCurrency: string = 'USD', + targetCurrency: string = "USD", ): string | null { const normalizedCurrency = targetCurrency.toUpperCase(); const amount = this.convertBchToFiat(satoshis, normalizedCurrency); @@ -195,7 +193,7 @@ export class RatesService extends EventEmitter { /** * Handles normalized updates from the underlying adapter. */ - private handleRateUpdated(event: RatesEventMap['rateUpdated']): void { + private handleRateUpdated(event: RatesEventMap["rateUpdated"]): void { const numeratorUnitCode = event.numeratorUnitCode.toUpperCase(); const denominatorUnitCode = event.denominatorUnitCode.toUpperCase(); const pair = this.getPairKey(numeratorUnitCode, denominatorUnitCode); @@ -206,7 +204,7 @@ export class RatesService extends EventEmitter { updatedAt, }); - this.emit('rate-updated', { + this.emit("rate-updated", { numeratorUnitCode, denominatorUnitCode, price: event.price, diff --git a/src/services/settings.ts b/src/services/settings.ts index ed8fa5e..2cbaab8 100644 --- a/src/services/settings.ts +++ b/src/services/settings.ts @@ -168,7 +168,9 @@ export class SettingsService extends EventEmitter { return normalized; } - const maybeMnemonic = (input as Record)["default-mnemonic"]; + const maybeMnemonic = (input as Record)[ + "default-mnemonic" + ]; if (typeof maybeMnemonic === "string" && maybeMnemonic.trim().length > 0) { normalized["default-mnemonic"] = maybeMnemonic.trim(); } diff --git a/src/services/storage.ts b/src/services/storage.ts index 8e55947..3e62c5f 100644 --- a/src/services/storage.ts +++ b/src/services/storage.ts @@ -142,7 +142,7 @@ export class Storage extends BaseStorage { * * This adapter is useful for tests and short-lived sessions where persisted * SQLite state is not needed. - * + * * TODO: Move this somewhere else. There is no reason for this to be in the main codebase. We should put this stricly in the tests beacuse that were its actually being used. * Ideally, we would provide these kind of generic fills as part of our packages somewhere, but these interfaces dont fit our current design. */ diff --git a/src/templates/vending-machine.ts b/src/templates/vending-machine.ts index 48c6600..2511813 100644 --- a/src/templates/vending-machine.ts +++ b/src/templates/vending-machine.ts @@ -1,4 +1,4 @@ -import type { XOTemplate } from '@xo-cash/types'; +import type { XOTemplate } from "@xo-cash/types"; /** * Vending machine payment template. @@ -7,271 +7,277 @@ import type { XOTemplate } from '@xo-cash/types'; * customer funds and signs the composable transaction. */ export const vendingMachineTemplate: XOTemplate = { - $schema: 'https://libauth.org/schemas/wallet-template-v0.schema.json', - name: 'Vending Machine', - description: 'Purchase items from a vending machine with an itemized receipt.', - icon: 'wallet', - version: '1', - supported: ['BCH_2023_05', 'BCH_2024_05', 'BCH_2025_05', 'BCH_2026_05'], + $schema: "https://libauth.org/schemas/wallet-template-v0.schema.json", + name: "Vending Machine", + description: + "Purchase items from a vending machine with an itemized receipt.", + icon: "wallet", + version: "1", + supported: ["BCH_2023_05", "BCH_2024_05", "BCH_2025_05", "BCH_2026_05"], - defaults: { - change: { - output: 'changeOutput', - role: 'merchant', - generate: ['merchantKey'], - }, + defaults: { + change: { + output: "changeOutput", + role: "merchant", + generate: ["merchantKey"], }, + }, - roles: { + roles: { + merchant: { + name: "Merchant", + description: "The vending machine operator receiving payment.", + icon: "owner", + }, + customer: { + name: "Customer", + description: "The customer paying for items.", + icon: "sender", + }, + }, + + start: [ + { + action: "purchaseItems", + role: "merchant", + generate: ["merchantKey"], + }, + ], + + actions: { + purchaseItems: { + name: "Purchase Items", + description: "Purchase: $() for $() sats", + icon: "request", + + roles: { merchant: { - name: 'Merchant', - description: 'The vending machine operator receiving payment.', - icon: 'owner', + name: "Sell Items", + description: "Receive payment for $()", + icon: "request", + requirements: { + secrets: ["merchantKey"], + variables: [ + "totalSatoshis", + "orderId", + "merchantName", + "receiptSummary", + "lineItemsJson", + ], + }, }, customer: { - name: 'Customer', - description: 'The customer paying for items.', - icon: 'sender', + name: "Pay", + description: "Pay $() sats for $()", + icon: "send", + requirements: {}, }, - }, + }, - start: [ + requirements: { + participants: [ + { role: "merchant", slots: { min: 1, max: 1 } }, + { role: "customer", slots: { min: 1 } }, + ], + }, + + transaction: "purchaseItemsTransaction", + }, + }, + + transactions: { + purchaseItemsTransaction: { + name: "Vending Purchase", + description: "Order $(): $()", + icon: "request", + + roles: { + merchant: { + name: "Received Payment", + description: + "Received $() sats from $() sale", + icon: "receive", + }, + customer: { + name: "Sent Payment", + description: "Paid $() sats for $()", + icon: "send", + }, + }, + + inputs: [], + outputs: [{ output: "purchaseOutput" }], + version: 2, + locktime: 0, + composable: true, + }, + }, + + /** No custom input templates — customer UTXOs are selected at funding time. */ + inputs: {}, + + outputs: { + changeOutput: { + name: "Change", + description: "Funds returned as change.", + icon: "receive", + lockingScript: "merchantReceivingLockingScript", + }, + purchaseOutput: { + name: "Purchase Payment", + description: "$() sats to $()", + icon: "request", + + roles: { + merchant: { + name: "Payment Received", + description: + "Received $() sats for $()", + }, + customer: { + name: "Payment Sent", + description: "Sent $() sats for $()", + }, + }, + + lockingScript: "merchantReceivingLockingScript", + valueSatoshis: "$()", + token: null, + }, + }, + + lockingScripts: { + merchantReceivingLockingScript: { + name: "Merchant Receive", + description: "Funds received by the vending machine merchant.", + icon: "address", + lockingType: "p2pkh", + lockingBytecode: "lockMerchantP2PKH", + unlockingBytecode: "unlockMerchantP2PKH", + actions: [], + state: { variables: [], secrets: [] }, + balance: {}, + roles: { + merchant: { + state: { + variables: [], + secrets: ["merchantKey"], + }, + actions: [], + balance: { + satoshis: true, + fungibleTokens: true, + nonfungibleTokens: true, + }, + selectable: true, + }, + }, + }, + }, + + scripts: { + lockMerchantP2PKH: + "OP_DUP OP_HASH160 <$( OP_HASH160)> OP_EQUALVERIFY OP_CHECKSIG", + unlockMerchantP2PKH: + " ", + }, + + constants: { + dustLimit: { + name: "Dust Limit", + description: "Minimum satoshis for P2PKH outputs.", + type: "integer", + value: 546, + }, + }, + + variables: { + merchantKey: { + name: "Merchant Private Key", + description: "Private key for the vending machine merchant wallet.", + type: "bytes", + hint: "private_key", + }, + totalSatoshis: { + name: "Total Price", + description: "Total purchase price in satoshis", + type: "integer", + hint: "satoshis", + }, + orderId: { + name: "Order ID", + description: "Unique order identifier", + type: "string", + }, + merchantName: { + name: "Merchant Name", + description: "Display name of the vending machine", + type: "string", + }, + receiptSummary: { + name: "Receipt Summary", + description: "Human-readable list of purchased items", + type: "string", + }, + lineItemsJson: { + name: "Line Items", + description: "JSON-encoded line items for the purchase", + type: "string", + }, + }, + + icons: [ + { name: "wallet", hash: "0000000000000000000000" }, + { name: "owner", hash: "0000000000000000000000" }, + { name: "sender", hash: "0000000000000000000000" }, + { name: "request", hash: "0000000000000000000000" }, + { name: "receive", hash: "0000000000000000000000" }, + { name: "send", hash: "0000000000000000000000" }, + ], + + scenarios: [ + { + name: "purchase items happy path", + description: "Merchant requests payment for vending machine items.", + action: "purchaseItems", + roles: [ { - action: 'purchaseItems', - role: 'merchant', - generate: ['merchantKey'], - }, - ], - - actions: { - purchaseItems: { - name: 'Purchase Items', - description: 'Purchase: $() for $() sats', - icon: 'request', - - roles: { - merchant: { - name: 'Sell Items', - description: 'Receive payment for $()', - icon: 'request', - requirements: { - secrets: ['merchantKey'], - variables: [ - 'totalSatoshis', - 'orderId', - 'merchantName', - 'receiptSummary', - 'lineItemsJson', - ], - }, - }, - customer: { - name: 'Pay', - description: 'Pay $() sats for $()', - icon: 'send', - requirements: {}, - }, + role: "merchant", + values: { + generated: { + merchantKey: + "KyRQa5pEXuzVcDwnXRLpYAascjchQW5DoxVRMbj4DTxS83573mz8", }, - - requirements: { - participants: [ - { role: 'merchant', slots: { min: 1, max: 1 } }, - { role: 'customer', slots: { min: 1 } }, - ], + variables: { + totalSatoshis: 3500, + orderId: "order-demo-1", + merchantName: "XO Snack Machine", + receiptSummary: "2× Cola, 1× Chips", + lineItemsJson: + '[{"name":"Cola","qty":2},{"name":"Chips","qty":1}]', }, - - transaction: 'purchaseItemsTransaction', - }, - }, - - transactions: { - purchaseItemsTransaction: { - name: 'Vending Purchase', - description: 'Order $(): $()', - icon: 'request', - - roles: { - merchant: { - name: 'Received Payment', - description: 'Received $() sats from $() sale', - icon: 'receive', - }, - customer: { - name: 'Sent Payment', - description: 'Paid $() sats for $()', - icon: 'send', - }, - }, - + secrets: {}, inputs: [], - outputs: [{ output: 'purchaseOutput' }], - version: 2, - locktime: 0, - composable: true, - }, - }, - - /** No custom input templates — customer UTXOs are selected at funding time. */ - inputs: {}, - - outputs: { - changeOutput: { - name: 'Change', - description: 'Funds returned as change.', - icon: 'receive', - lockingScript: 'merchantReceivingLockingScript', - }, - purchaseOutput: { - name: 'Purchase Payment', - description: '$() sats to $()', - icon: 'request', - - roles: { - merchant: { - name: 'Payment Received', - description: 'Received $() sats for $()', - }, - customer: { - name: 'Payment Sent', - description: 'Sent $() sats for $()', - }, - }, - - lockingScript: 'merchantReceivingLockingScript', - valueSatoshis: '$()', - token: null, - }, - }, - - lockingScripts: { - merchantReceivingLockingScript: { - name: 'Merchant Receive', - description: 'Funds received by the vending machine merchant.', - icon: 'address', - lockingType: 'p2pkh', - lockingBytecode: 'lockMerchantP2PKH', - unlockingBytecode: 'unlockMerchantP2PKH', - actions: [], - state: { variables: [], secrets: [] }, - balance: {}, - roles: { - merchant: { - state: { - variables: [], - secrets: ['merchantKey'], - }, - actions: [], - balance: { - satoshis: true, - fungibleTokens: true, - nonfungibleTokens: true, - }, - selectable: true, - }, - }, - }, - }, - - scripts: { - lockMerchantP2PKH: - 'OP_DUP OP_HASH160 <$( OP_HASH160)> OP_EQUALVERIFY OP_CHECKSIG', - unlockMerchantP2PKH: - ' ', - }, - - constants: { - dustLimit: { - name: 'Dust Limit', - description: 'Minimum satoshis for P2PKH outputs.', - type: 'integer', - value: 546, - }, - }, - - variables: { - merchantKey: { - name: 'Merchant Private Key', - description: 'Private key for the vending machine merchant wallet.', - type: 'bytes', - hint: 'private_key', - }, - totalSatoshis: { - name: 'Total Price', - description: 'Total purchase price in satoshis', - type: 'integer', - hint: 'satoshis', - }, - orderId: { - name: 'Order ID', - description: 'Unique order identifier', - type: 'string', - }, - merchantName: { - name: 'Merchant Name', - description: 'Display name of the vending machine', - type: 'string', - }, - receiptSummary: { - name: 'Receipt Summary', - description: 'Human-readable list of purchased items', - type: 'string', - }, - lineItemsJson: { - name: 'Line Items', - description: 'JSON-encoded line items for the purchase', - type: 'string', - }, - }, - - icons: [ - { name: 'wallet', hash: '0000000000000000000000' }, - { name: 'owner', hash: '0000000000000000000000' }, - { name: 'sender', hash: '0000000000000000000000' }, - { name: 'request', hash: '0000000000000000000000' }, - { name: 'receive', hash: '0000000000000000000000' }, - { name: 'send', hash: '0000000000000000000000' }, - ], - - scenarios: [ - { - name: 'purchase items happy path', - description: 'Merchant requests payment for vending machine items.', - action: 'purchaseItems', - roles: [ - { - role: 'merchant', - values: { - generated: { - merchantKey: 'KyRQa5pEXuzVcDwnXRLpYAascjchQW5DoxVRMbj4DTxS83573mz8', - }, - variables: { - totalSatoshis: 3500, - orderId: 'order-demo-1', - merchantName: 'XO Snack Machine', - receiptSummary: '2× Cola, 1× Chips', - lineItemsJson: '[{"name":"Cola","qty":2},{"name":"Chips","qty":1}]', - }, - secrets: {}, - inputs: [], - outputs: [ - { - lockingBytecode: '76a91475c715ecb74178fe87933e57e947e5e92d904b8188ac', - valueSatoshis: 3500, - }, - ], - }, - }, - { - role: 'customer', - values: { - generated: {}, - variables: {}, - secrets: {}, - inputs: [], - outputs: [], - }, - }, + outputs: [ + { + lockingBytecode: + "76a91475c715ecb74178fe87933e57e947e5e92d904b8188ac", + valueSatoshis: 3500, + }, ], + }, }, - ], + { + role: "customer", + values: { + generated: {}, + variables: {}, + secrets: {}, + inputs: [], + outputs: [], + }, + }, + ], + }, + ], }; diff --git a/src/templates/wrap-template.ts b/src/templates/wrap-template.ts index dc1befd..23d14c2 100644 --- a/src/templates/wrap-template.ts +++ b/src/templates/wrap-template.ts @@ -1,266 +1,270 @@ -import type { XOTemplate } from '@xo-cash/types'; +import type { XOTemplate } from "@xo-cash/types"; export const wrapBCHTemplate: XOTemplate = { - $schema: 'https://libauth.org/schemas/wallet-template-v0.schema.json', + $schema: "https://libauth.org/schemas/wallet-template-v0.schema.json", - name: 'Wrapped BCH', - description: 'Convert between BCH and wBCH tokens.', - icon: 'wrap', + name: "Wrapped BCH", + description: "Convert between BCH and wBCH tokens.", + icon: "wrap", - version: '1', - supported: ['BCH_2023_05', 'BCH_2024_05', 'BCH_2025_05', 'BCH_2026_05'], + version: "1", + supported: ["BCH_2023_05", "BCH_2024_05", "BCH_2025_05", "BCH_2026_05"], - roles: { - user: { - name: 'User', - description: 'The person wrapping or unwrapping BCH.', - icon: 'user', - }, - }, + roles: { + user: { + name: "User", + description: "The person wrapping or unwrapping BCH.", + icon: "user", + }, + }, - start: [ - { - action: 'wrap', - role: 'user', - }, - { - action: 'unwrap', - role: 'user', - }, - ], + start: [ + { + action: "wrap", + role: "user", + }, + { + action: "unwrap", + role: "user", + }, + ], - actions: { - wrap: { - name: 'Wrap BCH', - description: 'Convert BCH into wBCH tokens.', - icon: 'wrap', + actions: { + wrap: { + name: "Wrap BCH", + description: "Convert BCH into wBCH tokens.", + icon: "wrap", - roles: { - user: { - requirements: { - variables: ['amountToWrap', 'recipientLockingScript'], - }, - }, - }, + roles: { + user: { + requirements: { + variables: ["amountToWrap", "recipientLockingScript"], + }, + }, + }, - requirements: { - participants: [{ role: 'user', slots: { min: 1, max: 1 } }], - }, + requirements: { + participants: [{ role: "user", slots: { min: 1, max: 1 } }], + }, - transaction: 'wrapTransaction', - }, + transaction: "wrapTransaction", + }, - unwrap: { - name: 'Unwrap wBCH', - description: 'Convert wBCH tokens back into BCH.', - icon: 'unwrap', + unwrap: { + name: "Unwrap wBCH", + description: "Convert wBCH tokens back into BCH.", + icon: "unwrap", - roles: { - user: { - requirements: { - variables: ['amountToUnwrap', 'recipientLockingScript'], - }, - }, - }, + roles: { + user: { + requirements: { + variables: ["amountToUnwrap", "recipientLockingScript"], + }, + }, + }, - requirements: { - participants: [{ role: 'user', slots: { min: 1, max: 1 } }], - }, + requirements: { + participants: [{ role: "user", slots: { min: 1, max: 1 } }], + }, - transaction: 'unwrapTransaction', - }, - }, + transaction: "unwrapTransaction", + }, + }, - transactions: { - wrapTransaction: { - name: 'Wrapped BCH', - description: 'Wrapped $( OP_DIV).$( OP_MOD) BCH into wBCH tokens.', - icon: 'wrap', + transactions: { + wrapTransaction: { + name: "Wrapped BCH", + description: + "Wrapped $( OP_DIV).$( OP_MOD) BCH into wBCH tokens.", + icon: "wrap", - inputs: [ - { input: 'covenantInput', inputIndex: 0 }, - ], - outputs: [ - { output: 'covenantOutput', outputIndex: 0 }, - { output: 'wrappedTokensOutput', outputIndex: undefined }, - ], + inputs: [{ input: "covenantInput", inputIndex: 0 }], + outputs: [ + { output: "covenantOutput", outputIndex: 0 }, + { output: "wrappedTokensOutput", outputIndex: undefined }, + ], - version: 2, - locktime: 0, - composable: true, - }, + version: 2, + locktime: 0, + composable: true, + }, - unwrapTransaction: { - name: 'Unwrapped wBCH', - description: 'Unwrapped $( OP_DIV).$( OP_MOD) wBCH tokens back into BCH.', - icon: 'unwrap', + unwrapTransaction: { + name: "Unwrapped wBCH", + description: + "Unwrapped $( OP_DIV).$( OP_MOD) wBCH tokens back into BCH.", + icon: "unwrap", - inputs: [ - { input: 'covenantInput', inputIndex: 0 }, - ], - outputs: [ - { output: 'covenantOutput', outputIndex: 0 }, - { output: 'unwrappedSatoshisOutput', outputIndex: undefined }, - ], + inputs: [{ input: "covenantInput", inputIndex: 0 }], + outputs: [ + { output: "covenantOutput", outputIndex: 0 }, + { output: "unwrappedSatoshisOutput", outputIndex: undefined }, + ], - version: 2, - locktime: 0, - composable: true, - }, - }, + version: 2, + locktime: 0, + composable: true, + }, + }, - outputs: { - covenantOutput: { - name: 'wBCH Covenant', - description: 'Holds BCH and wBCH tokens that can be freely converted.', - icon: 'contract', + outputs: { + covenantOutput: { + name: "wBCH Covenant", + description: "Holds BCH and wBCH tokens that can be freely converted.", + icon: "contract", - lockingScript: 'wrapBCHLockingScript', - }, + lockingScript: "wrapBCHLockingScript", + }, - wrappedTokensOutput: { - name: 'Wrapped wBCH', - description: 'Wrapped $( OP_DIV).$( OP_MOD) wBCH tokens.', - icon: 'receive', + wrappedTokensOutput: { + name: "Wrapped wBCH", + description: + "Wrapped $( OP_DIV).$( OP_MOD) wBCH tokens.", + icon: "receive", - valueSatoshis: '$()', - token: { - category: '$()', - amount: '$()', - nft: null, - }, + valueSatoshis: "$()", + token: { + category: "$()", + amount: "$()", + nft: null, + }, - roles: { - user: { - balance: { - satoshis: true, - fungibleTokens: true, - nonfungibleTokens: true, - }, - selectable: true, - }, - }, + roles: { + user: { + balance: { + satoshis: true, + fungibleTokens: true, + nonfungibleTokens: true, + }, + selectable: true, + }, + }, - lockingScript: '$()', - }, + lockingScript: "$()", + }, - unwrappedSatoshisOutput: { - name: 'Unwrapped BCH', - description: 'Unwrapped $( OP_DIV).$( OP_MOD) BCH.', - icon: 'receive', + unwrappedSatoshisOutput: { + name: "Unwrapped BCH", + description: + "Unwrapped $( OP_DIV).$( OP_MOD) BCH.", + icon: "receive", - valueSatoshis: '$()', - token: null, + valueSatoshis: "$()", + token: null, - roles: { - user: { - balance: { - satoshis: true, - fungibleTokens: true, - nonfungibleTokens: true, - }, - selectable: true, - }, - }, + roles: { + user: { + balance: { + satoshis: true, + fungibleTokens: true, + nonfungibleTokens: true, + }, + selectable: true, + }, + }, - lockingScript: '$()', - }, - }, + lockingScript: "$()", + }, + }, - inputs: { - covenantInput: { - name: 'wBCH Covenant', - description: 'The covenant being updated.', - icon: 'contract', + inputs: { + covenantInput: { + name: "wBCH Covenant", + description: "The covenant being updated.", + icon: "contract", - unlockingScript: 'unlockCovenant', - }, - }, + unlockingScript: "unlockCovenant", + }, + }, - lockingScripts: { - wrapBCHLockingScript: { - name: 'wBCH Covenant', - description: 'Holds BCH and wBCH tokens that can be freely converted.', - icon: 'contract', + lockingScripts: { + wrapBCHLockingScript: { + name: "wBCH Covenant", + description: "Holds BCH and wBCH tokens that can be freely converted.", + icon: "contract", - lockingType: 'p2sh', - lockingBytecode: 'wrapBCHLockingBytecode', + lockingType: "p2sh", + lockingBytecode: "wrapBCHLockingBytecode", - actions: [ - { action: 'wrap', role: 'user' }, - { action: 'unwrap', role: 'user' }, - ], + actions: [ + { action: "wrap", role: "user" }, + { action: "unwrap", role: "user" }, + ], - state: { - variables: [], - secrets: [], - }, - balance: { - satoshis: 0n, - fungibleTokens: 0n, - }, - selectable: false, - }, - }, + state: { + variables: [], + secrets: [], + }, + balance: { + satoshis: 0n, + fungibleTokens: 0n, + }, + selectable: false, + }, + }, - scripts: { - enforceCovenantPersists: 'OP_INPUTINDEX OP_DUP OP_OUTPUTBYTECODE OP_SWAP OP_UTXOBYTECODE OP_EQUAL OP_VERIFY', - enforceTokenCategoryPreserved: 'OP_INPUTINDEX OP_DUP OP_OUTPUTTOKENCATEGORY OP_SWAP OP_UTXOTOKENCATEGORY OP_EQUAL OP_VERIFY', - enforceValueTokenSumConserved: 'OP_INPUTINDEX OP_UTXOVALUE OP_INPUTINDEX OP_UTXOTOKENAMOUNT OP_ADD OP_INPUTINDEX OP_OUTPUTVALUE OP_INPUTINDEX OP_OUTPUTTOKENAMOUNT OP_ADD OP_EQUAL OP_VERIFY', + scripts: { + enforceCovenantPersists: + "OP_INPUTINDEX OP_DUP OP_OUTPUTBYTECODE OP_SWAP OP_UTXOBYTECODE OP_EQUAL OP_VERIFY", + enforceTokenCategoryPreserved: + "OP_INPUTINDEX OP_DUP OP_OUTPUTTOKENCATEGORY OP_SWAP OP_UTXOTOKENCATEGORY OP_EQUAL OP_VERIFY", + enforceValueTokenSumConserved: + "OP_INPUTINDEX OP_UTXOVALUE OP_INPUTINDEX OP_UTXOTOKENAMOUNT OP_ADD OP_INPUTINDEX OP_OUTPUTVALUE OP_INPUTINDEX OP_OUTPUTTOKENAMOUNT OP_ADD OP_EQUAL OP_VERIFY", - // Direct script references — introspection opcodes must not use $(...) evaluations - // because those are evaluated at compile time without transaction context. - wrapBCHLockingBytecode: 'enforceCovenantPersists enforceTokenCategoryPreserved enforceValueTokenSumConserved', - unlockCovenant: '', - }, + // Direct script references — introspection opcodes must not use $(...) evaluations + // because those are evaluated at compile time without transaction context. + wrapBCHLockingBytecode: + "enforceCovenantPersists enforceTokenCategoryPreserved enforceValueTokenSumConserved", + unlockCovenant: "", + }, - constants: { - wbchTokenCategory: { - name: 'wBCH Token Category', - description: 'The official token category for Wrapped BCH.', - type: 'bytes', - value: 'ff4d6e4b90aa8158d39c5dc874fd9411af1ac3b5ed6f354755e8362a0d02c6b3', - }, - satoshisPerBCH: { - name: 'Satoshis per BCH', - description: 'Used to display amounts in BCH with decimals.', - type: 'integer', - value: 100000000, - }, - tokenDust: { - name: 'Token Dust Limit', - description: 'Minimal satoshis required for a token-bearing output.', - type: 'integer', - value: 1000, - }, - }, + constants: { + wbchTokenCategory: { + name: "wBCH Token Category", + description: "The official token category for Wrapped BCH.", + type: "bytes", + value: "ff4d6e4b90aa8158d39c5dc874fd9411af1ac3b5ed6f354755e8362a0d02c6b3", + }, + satoshisPerBCH: { + name: "Satoshis per BCH", + description: "Used to display amounts in BCH with decimals.", + type: "integer", + value: 100000000, + }, + tokenDust: { + name: "Token Dust Limit", + description: "Minimal satoshis required for a token-bearing output.", + type: "integer", + value: 1000, + }, + }, - variables: { - amountToWrap: { - name: 'Amount to Wrap', - description: 'How much BCH to convert to wBCH (in satoshis).', - type: 'integer', - hint: 'satoshis', - }, - amountToUnwrap: { - name: 'Amount to Unwrap', - description: 'How much wBCH to convert back to BCH (in satoshis).', - type: 'integer', - hint: 'satoshis', - }, - recipientLockingScript: { - name: 'Destination', - description: 'Where to receive your BCH or wBCH tokens.', - type: 'bytes', - hint: 'lockingScript', - }, - }, + variables: { + amountToWrap: { + name: "Amount to Wrap", + description: "How much BCH to convert to wBCH (in satoshis).", + type: "integer", + hint: "satoshis", + }, + amountToUnwrap: { + name: "Amount to Unwrap", + description: "How much wBCH to convert back to BCH (in satoshis).", + type: "integer", + hint: "satoshis", + }, + recipientLockingScript: { + name: "Destination", + description: "Where to receive your BCH or wBCH tokens.", + type: "bytes", + hint: "lockingScript", + }, + }, - icons: [ - { name: 'wrap', hash: '0000000000000000000000' }, - { name: 'unwrap', hash: '0000000000000000000000' }, - { name: 'user', hash: '0000000000000000000000' }, - { name: 'contract', hash: '0000000000000000000000' }, - { name: 'receive', hash: '0000000000000000000000' }, - ], + icons: [ + { name: "wrap", hash: "0000000000000000000000" }, + { name: "unwrap", hash: "0000000000000000000000" }, + { name: "user", hash: "0000000000000000000000" }, + { name: "contract", hash: "0000000000000000000000" }, + { name: "receive", hash: "0000000000000000000000" }, + ], }; diff --git a/src/tui/utils/clipboard.ts b/src/tui/utils/clipboard.ts index 53bf7fd..6c54c4b 100644 --- a/src/tui/utils/clipboard.ts +++ b/src/tui/utils/clipboard.ts @@ -13,30 +13,36 @@ const execAsync = promisify(exec); // The command is a function that returns a promise that resolves to the result of the command. const clipboardMethods = { pbCopy: { - platform: (platform: string) => platform === 'darwin', - command: async (text: string) => execAsync(`printf '%s' '${text}' | pbcopy`), + platform: (platform: string) => platform === "darwin", + command: async (text: string) => + execAsync(`printf '%s' '${text}' | pbcopy`), }, xclip: { - platform: (platform: string) => platform === 'linux', - command: async (text: string) => execAsync(`printf '%s' '${text}' | xclip -selection clipboard`), + platform: (platform: string) => platform === "linux", + command: async (text: string) => + execAsync(`printf '%s' '${text}' | xclip -selection clipboard`), }, xsel: { - platform: (platform: string) => platform === 'linux', - command: async (text: string) => execAsync(`printf '%s' '${text}' | xsel --clipboard --input`), + platform: (platform: string) => platform === "linux", + command: async (text: string) => + execAsync(`printf '%s' '${text}' | xsel --clipboard --input`), }, ssh: { - platform: (platform: string) => platform === 'linux', - command: async (text: string) => process.stdout.write(`\x1b]52;c;${Buffer.from(text, 'utf-8').toString('base64')}\x07`), + platform: (platform: string) => platform === "linux", + command: async (text: string) => + process.stdout.write( + `\x1b]52;c;${Buffer.from(text, "utf-8").toString("base64")}\x07`, + ), }, clip: { - platform: (platform: string) => platform === 'windows', + platform: (platform: string) => platform === "windows", command: async (text: string) => execAsync(`echo|set /p="${text}" | clip`), }, clipboardy: { - platform: (platform: string) => platform === 'windows', + platform: (platform: string) => platform === "windows", command: async (text: string) => clipboardy.writeSync(text), }, -} +}; /** * Attempts to copy text to clipboard using multiple methods. @@ -51,7 +57,9 @@ export async function copyToClipboard(text: string): Promise { // Escape the text for shell commands const escapedText = text.replace(/'/g, "'\\''"); - const availableMethods = Object.values(clipboardMethods).filter(method => method.platform(platform)); + const availableMethods = Object.values(clipboardMethods).filter((method) => + method.platform(platform), + ); const errors: Error[] = []; @@ -63,7 +71,7 @@ export async function copyToClipboard(text: string): Promise { continue; } return; - } catch(error) { + } catch (error) { if (error instanceof Error) { errors.push(error); } @@ -71,5 +79,7 @@ export async function copyToClipboard(text: string): Promise { } // All methods failed - throw new Error(`Clipboard not available. ${errors.map(error => error.message).join('\n')}`); + throw new Error( + `Clipboard not available. ${errors.map((error) => error.message).join("\n")}`, + ); } diff --git a/src/tui/utils/list-directory-entries.ts b/src/tui/utils/list-directory-entries.ts index fa1bb1c..fc4649c 100644 --- a/src/tui/utils/list-directory-entries.ts +++ b/src/tui/utils/list-directory-entries.ts @@ -160,8 +160,7 @@ export function listDirectoryEntries( entries: [...entries, ...directories, ...files], }; } catch (error) { - const message = - error instanceof Error ? error.message : String(error); + const message = error instanceof Error ? error.message : String(error); return { entries: [], error: `Unable to read directory: ${message}`, diff --git a/src/utils/history-utils.ts b/src/utils/history-utils.ts index 2b727cc..c289609 100644 --- a/src/utils/history-utils.ts +++ b/src/utils/history-utils.ts @@ -51,7 +51,7 @@ export function buildHistoryDisplayRows( type: "history_output", label: output.outpoint ? `${output.outpoint.txid}:${output.outpoint.index}` - : output.outputIdentifier ?? "Output", + : (output.outputIdentifier ?? "Output"), description: `${item.template} | ${roles} | ${output.description}`, timestamp: item.createdAtTimestamp, isNested: false, @@ -96,7 +96,7 @@ export function buildHistoryDisplayRows( type: "history_output", label: output.outpoint ? `${output.outpoint.txid}:${output.outpoint.index}` - : output.outputIdentifier ?? "Output", + : (output.outputIdentifier ?? "Output"), description: output.description, isNested: true, valueSatoshis: output.valueSatoshis, diff --git a/src/utils/invitation-flow.ts b/src/utils/invitation-flow.ts index 65a35ac..7782e66 100644 --- a/src/utils/invitation-flow.ts +++ b/src/utils/invitation-flow.ts @@ -65,8 +65,18 @@ export const roleRequiresInputs = ( const actionRole = action.roles?.[roleIdentifier]; const actionRequirements = action.requirements; - const actionRoleRequirements = actionRole && actionRequirements && actionRequirements.participants?.find((participant) => participant.role === roleIdentifier); - const roleSlotsMin = actionRoleRequirements && actionRoleRequirements.slots && actionRoleRequirements.slots.min > 0 ? actionRoleRequirements.slots.min : 0; + const actionRoleRequirements = + actionRole && + actionRequirements && + actionRequirements.participants?.find( + (participant) => participant.role === roleIdentifier, + ); + const roleSlotsMin = + actionRoleRequirements && + actionRoleRequirements.slots && + actionRoleRequirements.slots.min > 0 + ? actionRoleRequirements.slots.min + : 0; if (roleSlotsMin > 0) return true; const transactionIdentifier = action.transaction; @@ -78,7 +88,6 @@ export const roleRequiresInputs = ( return (roleInputs?.length ?? 0) > 0; }; - export const getTransactionOutputIdentifier = ( output: XOTemplateTransactionOutput, ): string | undefined => { @@ -136,7 +145,8 @@ export const resolveProvidedLockingBytecodeHex = ( return undefined; } - const lockingScriptDefinition = template.lockingScripts?.[outputDefinition.lockingScript]; + const lockingScriptDefinition = + template.lockingScripts?.[outputDefinition.lockingScript]; const scriptIdentifier = lockingScriptDefinition?.lockingBytecode; if (!scriptIdentifier) return undefined; diff --git a/src/utils/load-template-from-file.ts b/src/utils/load-template-from-file.ts index aa76a58..33edc14 100644 --- a/src/utils/load-template-from-file.ts +++ b/src/utils/load-template-from-file.ts @@ -71,12 +71,7 @@ function resolveTemplateModuleLoaderPath(): string { } /** TypeScript extensions that require tsx to evaluate the template module. */ -const TYPESCRIPT_TEMPLATE_EXTENSIONS = new Set([ - ".ts", - ".tsx", - ".mts", - ".cts", -]); +const TYPESCRIPT_TEMPLATE_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts"]); /** * Loads a TS/JS template module in an isolated child process. @@ -155,7 +150,9 @@ async function loadTemplateModuleViaChildProcess( } if (stdout.trim().length === 0) { - reject(new TemplateLoadError("Template module loader returned no output.")); + reject( + new TemplateLoadError("Template module loader returned no output."), + ); return; } @@ -174,7 +171,9 @@ export async function loadTemplateFromFile(filePath: string): Promise { const absolutePath = path.resolve(filePath); if (!fs.existsSync(absolutePath)) { - throw new TemplateLoadError(`Template file does not exist: ${absolutePath}`); + throw new TemplateLoadError( + `Template file does not exist: ${absolutePath}`, + ); } const extension = path.extname(absolutePath).toLowerCase(); diff --git a/src/utils/paths.ts b/src/utils/paths.ts index 549fee2..eb004af 100644 --- a/src/utils/paths.ts +++ b/src/utils/paths.ts @@ -11,7 +11,8 @@ import { basename, isAbsolute, join, resolve } from "node:path"; * Base config directory. Created on first access. */ export function getConfigDir(): string { - const dir = process.env["XO_CONFIG_DIR"] || join(homedir(), ".config", "xo-cli"); + const dir = + process.env["XO_CONFIG_DIR"] || join(homedir(), ".config", "xo-cli"); mkdirSync(dir, { recursive: true }); return dir; } diff --git a/src/utils/pick-template-export.ts b/src/utils/pick-template-export.ts index 30bd144..88f1af5 100644 --- a/src/utils/pick-template-export.ts +++ b/src/utils/pick-template-export.ts @@ -6,7 +6,9 @@ * Returns true when `value` looks like an XOTemplate object (pre-schema check). * Used only to pick the correct export before {@link parseTemplate} validates fully. */ -export function isTemplateLike(value: unknown): value is Record { +export function isTemplateLike( + value: unknown, +): value is Record { if (value === null || typeof value !== "object" || Array.isArray(value)) { return false; } diff --git a/src/utils/rates/base-rates.ts b/src/utils/rates/base-rates.ts index 739278b..f3366d0 100644 --- a/src/utils/rates/base-rates.ts +++ b/src/utils/rates/base-rates.ts @@ -1,4 +1,4 @@ -import { EventEmitter } from '../event-emitter.js'; +import { EventEmitter } from "../event-emitter.js"; /** * Events emitted by our Rates Adapters @@ -44,14 +44,15 @@ export abstract class BaseRates< BCH: 8, USD: 2, }; - const minimumFractionDigits = minimumFractionDigitsMap[normalizedCurrency] ?? 2; + const minimumFractionDigits = + minimumFractionDigitsMap[normalizedCurrency] ?? 2; const maximumFractionDigits = Math.max(minimumFractionDigits, 8); try { - const formatter = new Intl.NumberFormat('en-US', { - style: 'currency', + const formatter = new Intl.NumberFormat("en-US", { + style: "currency", currency: normalizedCurrency, - currencyDisplay: 'narrowSymbol', + currencyDisplay: "narrowSymbol", minimumFractionDigits, maximumFractionDigits, }); @@ -61,7 +62,7 @@ export abstract class BaseRates< // Some numerator symbols from oracle pairs (e.g. DOGE/BCH) are not ISO-4217 // fiat currency codes, so Intl currency formatting will throw a RangeError. // In that case we still return a human-readable formatted value. - const numericFormatter = new Intl.NumberFormat('en-US', { + const numericFormatter = new Intl.NumberFormat("en-US", { minimumFractionDigits, maximumFractionDigits, }); diff --git a/src/utils/rates/rates-oracles.ts b/src/utils/rates/rates-oracles.ts index 12fb468..230701b 100644 --- a/src/utils/rates/rates-oracles.ts +++ b/src/utils/rates/rates-oracles.ts @@ -3,11 +3,11 @@ import { OracleMetadataMessage, OraclePriceMessage, type OracleMetadataMap, -} from '@generalprotocols/oracle-client'; +} from "@generalprotocols/oracle-client"; -import { type RatesEventMap, BaseRates } from './base-rates.js'; -import { type OffCallback } from '../event-emitter.js'; -import { SettingsService } from '../../services/settings.js'; +import { type RatesEventMap, BaseRates } from "./base-rates.js"; +import { type OffCallback } from "../event-emitter.js"; +import { SettingsService } from "../../services/settings.js"; // Add the Oracle Price Message to our Events for this Adapter. export type RatesOracleEventMap = RatesEventMap & { @@ -42,7 +42,7 @@ export class RatesOracle extends BaseRates { private started: boolean = false; private targetNumeratorUnitCode: string; - private targetDenominatorUnitCode: string = 'BCH'; + private targetDenominatorUnitCode: string = "BCH"; private unsubscribeFromSettings: OffCallback | null = null; public constructor(client: OracleClient, settings: SettingsService) { @@ -63,7 +63,7 @@ export class RatesOracle extends BaseRates { } this.started = true; this.unsubscribeFromSettings = this.settings.on( - 'settings-updated', + "settings-updated", this.handleSettingsUpdated.bind(this), ); @@ -150,7 +150,11 @@ export class RatesOracle extends BaseRates { this.handlePriceMessage(message); } } catch (error) { - console.error('Error refreshing prices for oracle:', oracle.publicKey, error); + console.error( + "Error refreshing prices for oracle:", + oracle.publicKey, + error, + ); } }), ); @@ -183,8 +187,10 @@ export class RatesOracle extends BaseRates { return; } - const sourceNumeratorUnitCode = oracle.SOURCE_NUMERATOR_UNIT_CODE.toUpperCase(); - const sourceDenominatorUnitCode = oracle.SOURCE_DENOMINATOR_UNIT_CODE.toUpperCase(); + const sourceNumeratorUnitCode = + oracle.SOURCE_NUMERATOR_UNIT_CODE.toUpperCase(); + const sourceDenominatorUnitCode = + oracle.SOURCE_DENOMINATOR_UNIT_CODE.toUpperCase(); // Only emit the pair currently selected in settings. if ( @@ -197,7 +203,7 @@ export class RatesOracle extends BaseRates { // Scale the price const priceValue = message.priceValue / oracle.ATTESTATION_SCALING; - this.emit('rateUpdated', { + this.emit("rateUpdated", { numeratorUnitCode: sourceNumeratorUnitCode, denominatorUnitCode: sourceDenominatorUnitCode, price: priceValue, @@ -208,13 +214,11 @@ export class RatesOracle extends BaseRates { /** * Tracks updates to settings and switches the actively emitted fiat pair. */ - private handleSettingsUpdated( - event: { - key: 'currency' | 'default-mnemonic'; - value: string | undefined; - }, - ) { - if (event.key !== 'currency' || !event.value) { + private handleSettingsUpdated(event: { + key: "currency" | "default-mnemonic"; + value: string | undefined; + }) { + if (event.key !== "currency" || !event.value) { return; } @@ -223,7 +227,7 @@ export class RatesOracle extends BaseRates { // Refresh so listeners get the latest value for the new currency quickly. if (this.started) { this.refreshPrices().catch((error) => { - console.error('Error refreshing prices after currency update:', error); + console.error("Error refreshing prices after currency update:", error); }); } } diff --git a/src/utils/template-module-loader.ts b/src/utils/template-module-loader.ts index f5ba077..649835d 100644 --- a/src/utils/template-module-loader.ts +++ b/src/utils/template-module-loader.ts @@ -27,8 +27,7 @@ try { const template = pickTemplateExport(loadedModule); process.stdout.write(serializeTemplate(template as XOTemplate)); } catch (error) { - const message = - error instanceof Error ? error.message : String(error); + const message = error instanceof Error ? error.message : String(error); console.error(`Failed to load template module: ${message}`); process.exit(1); } diff --git a/src/utils/template-utils.ts b/src/utils/template-utils.ts index 2d18a01..9aadf60 100644 --- a/src/utils/template-utils.ts +++ b/src/utils/template-utils.ts @@ -188,13 +188,13 @@ export function getRolesForAction( ); return startEntries.map((entry) => { - const roleDef = template.roles?.[entry.role || '']; + const roleDef = template.roles?.[entry.role || ""]; const roleObj = typeof roleDef === "object" ? roleDef : null; // TODO: This is ugly. Lot of conditionals. Need to take a much closer look at this. return { - roleId: entry.role || '', - name: roleObj?.name || entry.role || '', + roleId: entry.role || "", + name: roleObj?.name || entry.role || "", description: roleObj?.description, }; }); diff --git a/src/utils/utxo-metadata.ts b/src/utils/utxo-metadata.ts index e3829f4..636a9b6 100644 --- a/src/utils/utxo-metadata.ts +++ b/src/utils/utxo-metadata.ts @@ -9,7 +9,8 @@ export type UnspentOutputMetadata = { outputIdentifier?: string; }; -export type UnspentOutputWithMetadata = UnspentOutputData & UnspentOutputMetadata; +export type UnspentOutputWithMetadata = UnspentOutputData & + UnspentOutputMetadata; /** * Builds a lookup map from script hash to its stored metadata. diff --git a/tests/cli/autocomplete-completions.test.ts b/tests/cli/autocomplete-completions.test.ts index b74fe44..a786c6c 100644 --- a/tests/cli/autocomplete-completions.test.ts +++ b/tests/cli/autocomplete-completions.test.ts @@ -55,7 +55,7 @@ describe("shell completions", () => { test("uses shell-native mnemonic completion in fish", () => { const completions = generateFishCompletions("xo-cli"); - expect(completions).toContain("set -l config_dir \"$XO_CONFIG_DIR\""); + expect(completions).toContain('set -l config_dir "$XO_CONFIG_DIR"'); expect(completions).toContain("(__xo_cli_complete_mnemonics)"); expect(completions).not.toContain("(__xo_cli_complete_dynamic mnemonics)"); }); @@ -68,9 +68,9 @@ describe("shell completions", () => { const contents = readFileSync(configFile, "utf8"); expect(contents.match(/XO_CONFIG_DIR/g)).toHaveLength(2); - expect(contents.match(/eval "\$\(xo-cli completions bash\)"/g)).toHaveLength( - 1, - ); + expect( + contents.match(/eval "\$\(xo-cli completions bash\)"/g), + ).toHaveLength(1); }); test("adds a missing default without duplicating an existing loader", () => { @@ -79,16 +79,18 @@ describe("shell completions", () => { expect(installCompletions("bash", "xo-cli", configFile)).toBe(true); const contents = readFileSync(configFile, "utf8"); - expect(contents.match(/eval "\$\(xo-cli completions bash\)"/g)).toHaveLength( - 1, - ); + expect( + contents.match(/eval "\$\(xo-cli completions bash\)"/g), + ).toHaveLength(1); expect(contents).toContain( 'export XO_CONFIG_DIR="${XO_CONFIG_DIR:-$HOME/.config/xo-cli}"', ); }); test("preserves an existing custom config directory assignment", () => { - const configFile = createConfigFile("export XO_CONFIG_DIR=/tmp/custom-xo\n"); + const configFile = createConfigFile( + "export XO_CONFIG_DIR=/tmp/custom-xo\n", + ); expect(installCompletions("zsh", "xo-cli", configFile)).toBe(true); diff --git a/tests/cli/commands/settings.test.ts b/tests/cli/commands/settings.test.ts index 60a5925..366a23a 100644 --- a/tests/cli/commands/settings.test.ts +++ b/tests/cli/commands/settings.test.ts @@ -57,7 +57,9 @@ describe("settings command", () => { {}, ); - const persisted = JSON.parse(readFileSync(paths.walletConfigPath, "utf8")) as { + const persisted = JSON.parse( + readFileSync(paths.walletConfigPath, "utf8"), + ) as { currency: string; "default-mnemonic"?: string; }; diff --git a/tests/cli/commands/template.test.ts b/tests/cli/commands/template.test.ts index db3de1b..0031636 100644 --- a/tests/cli/commands/template.test.ts +++ b/tests/cli/commands/template.test.ts @@ -103,7 +103,7 @@ const testCases: TestCase[] = [ inputs: ["export", p2pkhTemplateIdentifier], shouldThrow: false, expectedData: {}, - logs: [{ out: "\"name\":\"Wallet (P2PKH)\"" }], + logs: [{ out: '"name":"Wallet (P2PKH)"' }], }, // Error cases - subcommand { diff --git a/tests/cli/mnemonic.test.ts b/tests/cli/mnemonic.test.ts index 03549c3..75c3b41 100644 --- a/tests/cli/mnemonic.test.ts +++ b/tests/cli/mnemonic.test.ts @@ -113,7 +113,9 @@ describe("mnemonic utilities", () => { // Due to some weird MacOS behavior we need to use realpathSync to get the correct path // Basically, tmpDir() returns a symlink, but moving to that path puts us at `/private/${tmpDir()}` - const expectedPath = realpathSync(path.join(tempDir, "mnemonic-relative")); + const expectedPath = realpathSync( + path.join(tempDir, "mnemonic-relative"), + ); // Compare to the expected path expect(resolved).toBe(expectedPath); diff --git a/tests/cli/mocks/engine.ts b/tests/cli/mocks/engine.ts index bfdb849..568d515 100644 --- a/tests/cli/mocks/engine.ts +++ b/tests/cli/mocks/engine.ts @@ -159,7 +159,9 @@ export const createMockEngine = async (seed: string) => { }; export const createMockAppService = async (engine: Engine) => { - const settings = new SettingsService(`${tmpdir()}/xo-cli-tests-settings.json`); + const settings = new SettingsService( + `${tmpdir()}/xo-cli-tests-settings.json`, + ); settings.setCurrency("USD"); const storage = await InMemoryStorage.create(); diff --git a/tests/cli/mocks/rates-service.ts b/tests/cli/mocks/rates-service.ts index 1237d71..81a04dc 100644 --- a/tests/cli/mocks/rates-service.ts +++ b/tests/cli/mocks/rates-service.ts @@ -5,7 +5,10 @@ export class MockRatesService extends BaseRates { super(); } - async getRate(numeratorUnitCode: string, denominatorUnitCode: string): Promise { + async getRate( + numeratorUnitCode: string, + denominatorUnitCode: string, + ): Promise { return 1; } @@ -20,4 +23,4 @@ export class MockRatesService extends BaseRates { async listPairs(): Promise> { return new Set(); } -} \ No newline at end of file +} diff --git a/tests/cli/mocks/template-p2pkh.ts b/tests/cli/mocks/template-p2pkh.ts index 0cb3033..ebc4df7 100644 --- a/tests/cli/mocks/template-p2pkh.ts +++ b/tests/cli/mocks/template-p2pkh.ts @@ -2,36 +2,37 @@ import type { XOTemplate } from "@xo-cash/types"; import { generateTemplateIdentifier, parseTemplate } from "@xo-cash/engine"; export const p2pkhTemplate: XOTemplate = { - $schema: 'https://libauth.org/schemas/wallet-template-v0.schema.json', + $schema: "https://libauth.org/schemas/wallet-template-v0.schema.json", // Name for this template. - name: 'Wallet (P2PKH)', + name: "Wallet (P2PKH)", // Description for this template. - description: 'A standard single-factor wallet template that uses Pay-to-Public-Key-Hash (P2PKH) locking scripts.', + description: + "A standard single-factor wallet template that uses Pay-to-Public-Key-Hash (P2PKH) locking scripts.", // Icon for this template. - icon: 'wallet', + icon: "wallet", // Version number for this template. - version: '1', + version: "1", // List of VM versions that can be used to run this template. - supported: [ 'BCH_2023_05', 'BCH_2024_05', 'BCH_2025_05', 'BCH_2026_05' ], + supported: ["BCH_2023_05", "BCH_2024_05", "BCH_2025_05", "BCH_2026_05"], // Sets optional default values to be used with this template. defaults: { - // Configures a default intent structure for creating change outputs and locking scripts. - // NOTE: This is used when the engine needs to make change for an output created by - // this template and there is no other policy provided elsewhere that takes precedence. - // NOTE: It is recommended that templates that create outputs with the 'selectable' property - // either provides a change policy here, or a comment that explain that they - // intentionally omit the change policy and why. - change: { - output: 'changeOutput', - role: 'receiver', - generate: [ 'ownerKey' ], - }, + // Configures a default intent structure for creating change outputs and locking scripts. + // NOTE: This is used when the engine needs to make change for an output created by + // this template and there is no other policy provided elsewhere that takes precedence. + // NOTE: It is recommended that templates that create outputs with the 'selectable' property + // either provides a change policy here, or a comment that explain that they + // intentionally omit the change policy and why. + change: { + output: "changeOutput", + role: "receiver", + generate: ["ownerKey"], + }, }, // Describe a list of roles that are used in this template. @@ -39,1402 +40,1457 @@ export const p2pkhTemplate: XOTemplate = { // For example, the same entity that acts as 'receiver' when creating/receiving an output // can later act as 'sender' (or 'owner') when performing a follow-up action. roles: { - owner: { - name: 'Wallet Owner', - description: 'The party who can spend from this wallet.', - icon: 'owner', - }, - receiver: { - name: 'Receiver', - description: 'A party that is receiving value.', - icon: 'receiver', - }, - sender: { - name: 'Sender', - description: 'A party that is sending value.', - icon: 'sender', - }, + owner: { + name: "Wallet Owner", + description: "The party who can spend from this wallet.", + icon: "owner", + }, + receiver: { + name: "Receiver", + description: "A party that is receiving value.", + icon: "receiver", + }, + sender: { + name: "Sender", + description: "A party that is sending value.", + icon: "sender", + }, }, // Define a list of entrypoints supported by this template. start: [ - { - action: 'receive', - role: 'receiver', - generate: [ 'ownerKey' ], - }, - { - action: 'requestSatoshis', - role: 'receiver', - generate: [ 'ownerKey' ], - }, - { - action: 'requestFungibleTokens', - role: 'receiver', - generate: [ 'ownerKey' ], - }, - { - action: 'requestNonfungibleTokens', - role: 'receiver', - generate: [ 'ownerKey' ], - }, + { + action: "receive", + role: "receiver", + generate: ["ownerKey"], + }, + { + action: "requestSatoshis", + role: "receiver", + generate: ["ownerKey"], + }, + { + action: "requestFungibleTokens", + role: "receiver", + generate: ["ownerKey"], + }, + { + action: "requestNonfungibleTokens", + role: "receiver", + generate: ["ownerKey"], + }, ], // Define a list of actions that can be taken by this template. // NOTE: There is no action to generate an address, but a wallet can create an invitation to a receive action and // extract the generated lockscript as needed as the engine will track all lockscripts it generates. actions: { - receive: { - // TODO: Consider rewriting to be generic/role-less. - name: 'Receive', - description: 'Receive an unspecified amount of cash and/or tokens from one or more senders.', - icon: 'receive', + receive: { + // TODO: Consider rewriting to be generic/role-less. + name: "Receive", + description: + "Receive an unspecified amount of cash and/or tokens from one or more senders.", + icon: "receive", - roles: { - receiver: { - name: 'Receive', - description: 'Receive an unspecified amount of cash and/or tokens from one or more senders.', - icon: 'receive', - - requirements: { - secrets: [ 'ownerKey' ], - }, - }, - sender: { - name: 'Send', - description: 'Send an unspecified amount of cash and/or tokens to the provided receiver.', - icon: 'send', - - // The sender only need to provide blockchain-level requirements. - // NOTE: This field is not required when empty, but shown here for illustrative purposes. - requirements: {}, - }, - }, + roles: { + receiver: { + name: "Receive", + description: + "Receive an unspecified amount of cash and/or tokens from one or more senders.", + icon: "receive", requirements: { - participants: [ - { - role: 'receiver', - slots: { min: 1, max: 1 }, - }, - { - role: 'sender', - slots: { min: 1, max: undefined }, - }, - ], - // variables: [ 'requestedSatoshis' ], + secrets: ["ownerKey"], }, + }, + sender: { + name: "Send", + description: + "Send an unspecified amount of cash and/or tokens to the provided receiver.", + icon: "send", - transaction: 'receiveTransaction', + // The sender only need to provide blockchain-level requirements. + // NOTE: This field is not required when empty, but shown here for illustrative purposes. + requirements: {}, + }, }, - requestSatoshis: { - // TODO: Consider rewriting to be generic/role-less. - name: 'Request Satoshis', - description: 'Requests a specific amount of Bitcoin Cash from one or more senders.', - icon: 'request', - roles: { - receiver: { - name: 'Request Satoshis', - description: 'Requests a specific amount of Bitcoin Cash from one or more senders.', - icon: 'request', - - requirements: { - secrets: [ 'ownerKey' ], - variables: [ 'requestedSatoshis' ], - }, - }, - sender: { - name: 'Send', - description: 'Send a specific amount of Bitcoin Cash to the provided receiver.', - icon: 'send', - - // The sender only need to provide blockchain-level requirements. - // NOTE: This field is not required when empty, but shown here for illustrative purposes. - requirements: {}, - }, + requirements: { + participants: [ + { + role: "receiver", + slots: { min: 1, max: 1 }, }, + { + role: "sender", + slots: { min: 1, max: undefined }, + }, + ], + // variables: [ 'requestedSatoshis' ], + }, + + transaction: "receiveTransaction", + }, + requestSatoshis: { + // TODO: Consider rewriting to be generic/role-less. + name: "Request Satoshis", + description: + "Requests a specific amount of Bitcoin Cash from one or more senders.", + icon: "request", + + roles: { + receiver: { + name: "Request Satoshis", + description: + "Requests a specific amount of Bitcoin Cash from one or more senders.", + icon: "request", requirements: { - participants: [ - { - role: 'receiver', - slots: { min: 1, max: 1 }, - }, - { - role: 'sender', - slots: { min: 1, max: undefined }, - }, - ], + secrets: ["ownerKey"], + variables: ["requestedSatoshis"], }, + }, + sender: { + name: "Send", + description: + "Send a specific amount of Bitcoin Cash to the provided receiver.", + icon: "send", - transaction: 'requestSatoshisTransaction', + // The sender only need to provide blockchain-level requirements. + // NOTE: This field is not required when empty, but shown here for illustrative purposes. + requirements: {}, + }, }, - requestFungibleTokens: { - // TODO: Consider rewriting to be generic/role-less. - name: 'Request Fungible Tokens', - description: 'Requests a specific amount of a fungible tokens from one or more senders.', - icon: 'request', - roles: { - receiver: { - name: 'Request Fungible Tokens', - description: 'Requests a specific amount of a fungible tokens from one or more senders.', - icon: 'request', - - requirements: { - secrets: [ 'ownerKey' ], - variables: [ 'requestedTokenCategory', 'requestedTokenAmount' ], - }, - }, - sender: { - name: 'Send', - description: 'Send a specific amount of fungible tokens to the provided receiver.', - icon: 'send', - - // The sender only need to provide blockchain-level requirements. - // NOTE: This field is not required when empty, but shown here for illustrative purposes. - requirements: {}, - }, + requirements: { + participants: [ + { + role: "receiver", + slots: { min: 1, max: 1 }, }, + { + role: "sender", + slots: { min: 1, max: undefined }, + }, + ], + }, + + transaction: "requestSatoshisTransaction", + }, + requestFungibleTokens: { + // TODO: Consider rewriting to be generic/role-less. + name: "Request Fungible Tokens", + description: + "Requests a specific amount of a fungible tokens from one or more senders.", + icon: "request", + + roles: { + receiver: { + name: "Request Fungible Tokens", + description: + "Requests a specific amount of a fungible tokens from one or more senders.", + icon: "request", requirements: { - participants: [ - { - role: 'receiver', - slots: { min: 1, max: 1 }, - }, - { - role: 'sender', - slots: { min: 1, max: undefined }, - }, - ], + secrets: ["ownerKey"], + variables: ["requestedTokenCategory", "requestedTokenAmount"], }, + }, + sender: { + name: "Send", + description: + "Send a specific amount of fungible tokens to the provided receiver.", + icon: "send", - transaction: 'requestFungibleTokensTransaction', + // The sender only need to provide blockchain-level requirements. + // NOTE: This field is not required when empty, but shown here for illustrative purposes. + requirements: {}, + }, }, - requestNonfungibleTokens: { - // TODO: Consider rewriting to be generic/role-less. - name: 'Request a Non-fungible Token', - description: 'Requests a non-fungible token from one or more senders.', - icon: 'request', - roles: { - receiver: { - name: 'Request a Non-fungible Token', - description: 'Requests a non-fungible token from one or more senders.', - icon: 'request', - - requirements: { - secrets: [ 'ownerKey' ], - variables: [ 'requestedTokenCategory', 'requestedTokenCapability', 'requestedTokenCommitment' ], - }, - }, - sender: { - name: 'Send', - description: 'Send a non-fungible token to the provided receiver.', - icon: 'send', - - // The sender only need to provide blockchain-level requirements. - // NOTE: This field is not required when empty, but shown here for illustrative purposes. - requirements: {}, - }, + requirements: { + participants: [ + { + role: "receiver", + slots: { min: 1, max: 1 }, }, + { + role: "sender", + slots: { min: 1, max: undefined }, + }, + ], + }, + + transaction: "requestFungibleTokensTransaction", + }, + requestNonfungibleTokens: { + // TODO: Consider rewriting to be generic/role-less. + name: "Request a Non-fungible Token", + description: "Requests a non-fungible token from one or more senders.", + icon: "request", + + roles: { + receiver: { + name: "Request a Non-fungible Token", + description: + "Requests a non-fungible token from one or more senders.", + icon: "request", requirements: { - participants: [ - { - role: 'receiver', - slots: { min: 1, max: 1 }, - }, - { - role: 'sender', - slots: { min: 1, max: undefined }, - }, - ], + secrets: ["ownerKey"], + variables: [ + "requestedTokenCategory", + "requestedTokenCapability", + "requestedTokenCommitment", + ], }, + }, + sender: { + name: "Send", + description: "Send a non-fungible token to the provided receiver.", + icon: "send", - transaction: 'requestNonfungibleTokensTransaction', + // The sender only need to provide blockchain-level requirements. + // NOTE: This field is not required when empty, but shown here for illustrative purposes. + requirements: {}, + }, }, - // NOTE: Sending value can be done without explicit template support. - // NOTE: This feature is explicitly defined in this template to demonstrate how versatile templates can be, and - // to ensure the feature is discoverable by the user from outputs that hold value. - sendSatoshis: { - name: 'Send Satoshis', - description: 'Sends a specific amount of Bitcoin Cash to a given recipient.', - icon: 'send', - - roles: { - sender: { - requirements: { - variables: [ 'transferredSatoshis', 'recipientLockingscript' ], - secrets: [ 'ownerKey' ], - }, - }, + requirements: { + participants: [ + { + role: "receiver", + slots: { min: 1, max: 1 }, }, + { + role: "sender", + slots: { min: 1, max: undefined }, + }, + ], + }, + transaction: "requestNonfungibleTokensTransaction", + }, + + // NOTE: Sending value can be done without explicit template support. + // NOTE: This feature is explicitly defined in this template to demonstrate how versatile templates can be, and + // to ensure the feature is discoverable by the user from outputs that hold value. + sendSatoshis: { + name: "Send Satoshis", + description: + "Sends a specific amount of Bitcoin Cash to a given recipient.", + icon: "send", + + roles: { + sender: { requirements: { - participants: [ - { - role: 'sender', - slots: { min: 1, max: 1 }, - }, - ], + variables: ["transferredSatoshis", "recipientLockingscript"], + secrets: ["ownerKey"], }, - - // Sending is only available for outputs that have sufficient satoshis on them. - // NOTE: Dust is enforced here according to standardness rules. - conditions: [ '$(OP_INPUTINDEX OP_UTXOVALUE OP_GREATERTHAN)' ], - - transaction: 'transferSatoshisTransaction', + }, }, - sendFungibleTokens: { - name: 'Send Fungible Tokens', - description: 'Send a specific amount of a fungible token to a given recipient.', - icon: 'send', - roles: { - sender: { - requirements: { - variables: [ 'transferredTokenCategory', 'transferredTokenAmount', 'recipientLockingscript' ], - secrets: [ 'ownerKey' ], - }, - }, + requirements: { + participants: [ + { + role: "sender", + slots: { min: 1, max: 1 }, }, + ], + }, + // Sending is only available for outputs that have sufficient satoshis on them. + // NOTE: Dust is enforced here according to standardness rules. + conditions: ["$(OP_INPUTINDEX OP_UTXOVALUE OP_GREATERTHAN)"], + + transaction: "transferSatoshisTransaction", + }, + sendFungibleTokens: { + name: "Send Fungible Tokens", + description: + "Send a specific amount of a fungible token to a given recipient.", + icon: "send", + + roles: { + sender: { requirements: { - participants: [ - { - role: 'sender', - slots: { min: 1, max: 1 }, - }, - ], + variables: [ + "transferredTokenCategory", + "transferredTokenAmount", + "recipientLockingscript", + ], + secrets: ["ownerKey"], }, - - // Sending is only available for outputs that have fungible tokens on them. - conditions: [ '$(OP_INPUTINDEX OP_UTXOTOKENAMOUNT <0> OP_GREATERTHAN)' ], - - transaction: 'transferFungibleTokensTransaction', + }, }, - sendNonfungibleTokens: { - name: 'Send a Non-fungible Token', - description: 'Send a non-fungible token to a given recipient.', - icon: 'send', - roles: { - sender: { - requirements: { - variables: [ 'transferredTokenCategory', 'transferredTokenCapability', 'transferredTokenCommitment', 'recipientLockingscript' ], - secrets: [ 'ownerKey' ], - }, - }, + requirements: { + participants: [ + { + role: "sender", + slots: { min: 1, max: 1 }, }, + ], + }, + // Sending is only available for outputs that have fungible tokens on them. + conditions: ["$(OP_INPUTINDEX OP_UTXOTOKENAMOUNT <0> OP_GREATERTHAN)"], + + transaction: "transferFungibleTokensTransaction", + }, + sendNonfungibleTokens: { + name: "Send a Non-fungible Token", + description: "Send a non-fungible token to a given recipient.", + icon: "send", + + roles: { + sender: { requirements: { - participants: [ - { - role: 'sender', - slots: { min: 1, max: 1 }, - }, - ], + variables: [ + "transferredTokenCategory", + "transferredTokenCapability", + "transferredTokenCommitment", + "recipientLockingscript", + ], + secrets: ["ownerKey"], }, - - // Sending is only available for outputs that have a non-fungible token on them. - conditions: [ '$(OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_SIZE OP_NIP <32> OP_GREATERTHAN)' ], - - transaction: 'transferNonfungibleTokensTransaction', + }, }, - // NOTE: Burning tokens can be done without explicit template support. - // NOTE: This feature is explicitly defined in this template to demonstrate how versatile templates can be, and - // to ensure the feature is discoverable by the user from outputs that hold tokens. - burnFungibleTokens: { - name: 'Delete Fungible Tokens', - description: 'Permanently and irreversibly deletes one or more fungible tokens.', - icon: 'burn', - - roles: { - owner: { - requirements: { - variables: [ 'burnedTokenCategory', 'burnedTokenAmount' ], - secrets: [ 'ownerKey' ], - }, - }, + requirements: { + participants: [ + { + role: "sender", + slots: { min: 1, max: 1 }, }, + ], + }, + // Sending is only available for outputs that have a non-fungible token on them. + conditions: [ + "$(OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_SIZE OP_NIP <32> OP_GREATERTHAN)", + ], + + transaction: "transferNonfungibleTokensTransaction", + }, + + // NOTE: Burning tokens can be done without explicit template support. + // NOTE: This feature is explicitly defined in this template to demonstrate how versatile templates can be, and + // to ensure the feature is discoverable by the user from outputs that hold tokens. + burnFungibleTokens: { + name: "Delete Fungible Tokens", + description: + "Permanently and irreversibly deletes one or more fungible tokens.", + icon: "burn", + + roles: { + owner: { requirements: { - participants: [ - { - role: 'owner', - slots: { min: 1, max: 1 }, - }, - ], + variables: ["burnedTokenCategory", "burnedTokenAmount"], + secrets: ["ownerKey"], }, - - // Burning is only available for outputs that have fungible tokens on them. - conditions: [ '$(OP_INPUTINDEX OP_UTXOTOKENAMOUNT <0> OP_GREATERTHAN)' ], - - transaction: 'burnFungibleTokensTransaction', + }, }, - burnNonfungibleTokens: { - name: 'Delete a Non-fungible Token', - description: 'Permanently and irreversibly deletes one non-fungible token.', - icon: 'burn', - roles: { - owner: { - requirements: { - variables: [ 'burnedTokenCategory', 'burnedTokenCapability', 'burnedTokenCommitment' ], - secrets: [ 'ownerKey' ], - }, - }, + requirements: { + participants: [ + { + role: "owner", + slots: { min: 1, max: 1 }, }, + ], + }, + // Burning is only available for outputs that have fungible tokens on them. + conditions: ["$(OP_INPUTINDEX OP_UTXOTOKENAMOUNT <0> OP_GREATERTHAN)"], + + transaction: "burnFungibleTokensTransaction", + }, + burnNonfungibleTokens: { + name: "Delete a Non-fungible Token", + description: + "Permanently and irreversibly deletes one non-fungible token.", + icon: "burn", + + roles: { + owner: { requirements: { - participants: [ - { - role: 'owner', - slots: { min: 1, max: 1 }, - }, - ], + variables: [ + "burnedTokenCategory", + "burnedTokenCapability", + "burnedTokenCommitment", + ], + secrets: ["ownerKey"], }, - - // Burning is only available for outputs that have non-fungible tokens on them. - conditions: [ '$(OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_SIZE OP_NIP <32> OP_GREATERTHAN)' ], - - transaction: 'burnNonfungibleTokenTransaction', + }, }, - sign: { - name: 'Sign Message', - description: 'Signs a provided message using the Bitcoin message signing protocol.', - icon: 'sign', - - roles: { - owner: { - requirements: { - variables: [ 'messageToSign' ], - secrets: [ 'ownerKey' ], - }, - }, + requirements: { + participants: [ + { + role: "owner", + slots: { min: 1, max: 1 }, }, + ], + }, + // Burning is only available for outputs that have non-fungible tokens on them. + conditions: [ + "$(OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_SIZE OP_NIP <32> OP_GREATERTHAN)", + ], + + transaction: "burnNonfungibleTokenTransaction", + }, + + sign: { + name: "Sign Message", + description: + "Signs a provided message using the Bitcoin message signing protocol.", + icon: "sign", + + roles: { + owner: { requirements: { - participants: [ - { - role: 'owner', - slots: { min: 1, max: 1 }, - }, - ], + variables: ["messageToSign"], + secrets: ["ownerKey"], }, - - data: 'messageSignature', + }, }, - verify: { - name: 'Verify Message Signature', - description: 'Verifies a provided message signature according to the Bitcoin message signing protocol.', - icon: 'verify', - roles: { - owner: { - requirements: { - variables: [ 'messageSignature', 'messageToVerify' ], - secrets: [ 'ownerKey' ], - }, - }, + requirements: { + participants: [ + { + role: "owner", + slots: { min: 1, max: 1 }, }, + ], + }, + data: "messageSignature", + }, + verify: { + name: "Verify Message Signature", + description: + "Verifies a provided message signature according to the Bitcoin message signing protocol.", + icon: "verify", + + roles: { + owner: { requirements: { - participants: [ - { - role: 'owner', - slots: { min: 1, max: 1 }, - }, - ], + variables: ["messageSignature", "messageToVerify"], + secrets: ["ownerKey"], }, - - data: 'messageSignatureValidity', + }, }, + + requirements: { + participants: [ + { + role: "owner", + slots: { min: 1, max: 1 }, + }, + ], + }, + + data: "messageSignatureValidity", + }, }, // Define a set of data that can be used in this template. data: { - messageSignature: { - // Evaluate CashASM expression to get the signature needed. - // NOTE: Pushes the prefix and message, then concatenates them together to form the data to sign. - // NOTE: In libauth today, it seems that this is done by defining the signature as a variable, and tying it to the key and message, - // and so this is different - // TODO: Check with Jason and see if there is any reason why this cannot be done like this. - value: '$( OP_CAT )', - type: 'bytes', - hint: 'signature', - }, - messageSignatureValidity: { - // Evaluate the validity of the message with the owners public key. - value: '$( OP_CAT OP_CHECKDATASIG)', - type: 'integer', - hint: 'script_boolean', - }, + messageSignature: { + // Evaluate CashASM expression to get the signature needed. + // NOTE: Pushes the prefix and message, then concatenates them together to form the data to sign. + // NOTE: In libauth today, it seems that this is done by defining the signature as a variable, and tying it to the key and message, + // and so this is different + // TODO: Check with Jason and see if there is any reason why this cannot be done like this. + value: + "$( OP_CAT )", + type: "bytes", + hint: "signature", + }, + messageSignatureValidity: { + // Evaluate the validity of the message with the owners public key. + value: + "$( OP_CAT OP_CHECKDATASIG)", + type: "integer", + hint: "script_boolean", + }, }, // Define a set of transactions that can be used in this template. transactions: { - receiveTransaction: { - name: 'Transfer Completed', - description: 'Transferred an unspecified amount of cash and/or tokens.', - icon: 'request', + receiveTransaction: { + name: "Transfer Completed", + description: "Transferred an unspecified amount of cash and/or tokens.", + icon: "request", - roles: { - receiver: { - name: 'Received', - description: 'Received an unspecified amount of cash and/or tokens.', - icon: 'receive', - }, - sender: { - name: 'Sent', - description: 'Sent an unspecified amount of cash and/or tokens.', - icon: 'send', - }, - }, - - // Inputs and outputs that must exist in the transaction. - // NOTE: There is no inputs required, but the engine should detect that there is not sufficient input value to - // match the output and thus generate an invitation to participate in this action. - // When the invitation is shared, the other parties can add as many inputs and change outputs as needed. - inputs: [], - outputs: [ - { - output: 'receiveOutput', - outputIndex: undefined, - }, - ], - - // Standard transaction without a locktime. - version: 2, - locktime: 0, - - // ... - composable: true, + roles: { + receiver: { + name: "Received", + description: "Received an unspecified amount of cash and/or tokens.", + icon: "receive", + }, + sender: { + name: "Sent", + description: "Sent an unspecified amount of cash and/or tokens.", + icon: "send", + }, }, - requestSatoshisTransaction: { - name: 'Satoshis Transferred', - description: 'Transferred $() satoshis.', - icon: 'request', - roles: { - receiver: { - name: 'Received', - description: 'Received $() satoshis.', - icon: 'receive', - }, - sender: { - name: 'Sent', - description: 'Sent $() satoshis.', - icon: 'send', - }, - }, + // Inputs and outputs that must exist in the transaction. + // NOTE: There is no inputs required, but the engine should detect that there is not sufficient input value to + // match the output and thus generate an invitation to participate in this action. + // When the invitation is shared, the other parties can add as many inputs and change outputs as needed. + inputs: [], + outputs: [ + { + output: "receiveOutput", + outputIndex: undefined, + }, + ], - // Inputs and outputs that must exist in the transaction. - // NOTE: There is no inputs required, but the engine should detect that there is not sufficient input value to - // match the output and thus generate an invitation to participate in this action. - // When the invitation is shared, the other party can add as many inputs and outputs as needed since this transaction is composable. - inputs: [], - outputs: [ - { - output: 'requestSatoshisOutput', - outputIndex: undefined, - }, - ], + // Standard transaction without a locktime. + version: 2, + locktime: 0, - // Standard transaction without a locktime. - version: 2, - locktime: 0, + // ... + composable: true, + }, + requestSatoshisTransaction: { + name: "Satoshis Transferred", + description: "Transferred $() satoshis.", + icon: "request", - // ... - composable: true, + roles: { + receiver: { + name: "Received", + description: "Received $() satoshis.", + icon: "receive", + }, + sender: { + name: "Sent", + description: "Sent $() satoshis.", + icon: "send", + }, }, - requestFungibleTokensTransaction: { - name: 'Fungible Tokens Transferred', + + // Inputs and outputs that must exist in the transaction. + // NOTE: There is no inputs required, but the engine should detect that there is not sufficient input value to + // match the output and thus generate an invitation to participate in this action. + // When the invitation is shared, the other party can add as many inputs and outputs as needed since this transaction is composable. + inputs: [], + outputs: [ + { + output: "requestSatoshisOutput", + outputIndex: undefined, + }, + ], + + // Standard transaction without a locktime. + version: 2, + locktime: 0, + + // ... + composable: true, + }, + requestFungibleTokensTransaction: { + name: "Fungible Tokens Transferred", + description: + "Transferred $( OP_DIV).$( OP_MOD) $() tokens.", + icon: "request", + + roles: { + receiver: { + name: "Received", description: - 'Transferred $( OP_DIV).$( OP_MOD) $() tokens.', - icon: 'request', - - roles: { - receiver: { - name: 'Received', - description: - 'Received $( OP_DIV).$( OP_MOD) $() tokens.', - icon: 'receive', - }, - sender: { - name: 'Sent', - description: - 'Sent $( OP_DIV).$( OP_MOD) $() tokens.', - icon: 'send', - }, - }, - - // Inputs and outputs that must exist in the transaction. - // NOTE: There is no inputs required, but the engine should detect that there is not sufficient input value to - // match the output and thus generate an invitation to participate in this action. - // When the invitation is shared, the other party can add as many inputs and outputs as needed since this transaction is composable. - inputs: [], - outputs: [ - { - output: 'requestFungibleTokensOutput', - outputIndex: undefined, - }, - ], - - // Standard transaction without a locktime. - version: 2, - locktime: 0, - - // ... - composable: true, - }, - requestNonfungibleTokensTransaction: { - name: 'Non-Fungible Token Transferred', + "Received $( OP_DIV).$( OP_MOD) $() tokens.", + icon: "receive", + }, + sender: { + name: "Sent", description: - 'Transferred one non-fungible $( <0x02> OP_EQUAL OP_IF <"minting"> OP_ELSE <0x01> OP_EQUAL OP_IF <"mutable"> OP_ELSE <"immutable"> OP_ENDIF OP_ENDIF) () token, with $() commitment.', - icon: 'request', - - roles: { - receiver: { - name: 'Received', - description: - 'Received one non-fungible $( <0x02> OP_EQUAL OP_IF <"minting"> OP_ELSE <0x01> OP_EQUAL OP_IF <"mutable"> OP_ELSE <"immutable"> OP_ENDIF OP_ENDIF) () token, with $() commitment.', - icon: 'receive', - }, - sender: { - name: 'Sent', - description: - 'Sent the requested non-fungible $( <0x02> OP_EQUAL OP_IF <"minting"> OP_ELSE <0x01> OP_EQUAL OP_IF <"mutable"> OP_ELSE <"immutable"> OP_ENDIF OP_ENDIF) () token, with $() commitment.', - icon: 'send', - }, - }, - - // Inputs and outputs that must exist in the transaction. - // NOTE: There is no inputs required, but the engine should detect that there is not sufficient input value to - // match the output and thus generate an invitation to participate in this action. - // When the invitation is shared, the other party can add as many inputs and outputs as needed since this transaction is composable. - inputs: [], - outputs: [ - { - output: 'requestNonfungibleTokensOutput', - outputIndex: undefined, - }, - ], - - // Standard transaction without a locktime. - version: 2, - locktime: 0, - - // ... - composable: true, + "Sent $( OP_DIV).$( OP_MOD) $() tokens.", + icon: "send", + }, }, - transferSatoshisTransaction: { - name: 'Satoshis Transferred', - description: '$() satoshis were transferred to a recipient.', - icon: 'send', + // Inputs and outputs that must exist in the transaction. + // NOTE: There is no inputs required, but the engine should detect that there is not sufficient input value to + // match the output and thus generate an invitation to participate in this action. + // When the invitation is shared, the other party can add as many inputs and outputs as needed since this transaction is composable. + inputs: [], + outputs: [ + { + output: "requestFungibleTokensOutput", + outputIndex: undefined, + }, + ], - roles: { - receiver: { - name: 'Received', - description: 'Received $() satoshis.', - icon: 'receive', - }, - sender: { - name: 'Sent', - description: 'Sent $() satoshis.', - icon: 'send', - }, - }, + // Standard transaction without a locktime. + version: 2, + locktime: 0, - // Enforce the inputs and outputs required by the transaction. - // NOTE: The input is provided from the action since it is only available on outputs with satoshis. - inputs: [], - outputs: [ - { - output: 'transferSatoshisOutput', - outputIndex: undefined, - }, - ], + // ... + composable: true, + }, + requestNonfungibleTokensTransaction: { + name: "Non-Fungible Token Transferred", + description: + 'Transferred one non-fungible $( <0x02> OP_EQUAL OP_IF <"minting"> OP_ELSE <0x01> OP_EQUAL OP_IF <"mutable"> OP_ELSE <"immutable"> OP_ENDIF OP_ENDIF) () token, with $() commitment.', + icon: "request", - // Standard transaction without a locktime. - version: 2, - locktime: 0, - - // ... - composable: true, - }, - transferFungibleTokensTransaction: { - name: 'Fungible Tokens Transferred', + roles: { + receiver: { + name: "Received", description: - '$( OP_DIV).$( OP_MOD) $() tokens were transferred to a recipient.', - icon: 'send', - - roles: { - receiver: { - name: 'Received', - description: - 'Received $( OP_DIV).$( OP_MOD) $() tokens.', - icon: 'receive', - }, - sender: { - name: 'Sent', - description: - 'Sent $( OP_DIV).$( OP_MOD) $() tokens.', - icon: 'send', - }, - }, - - // Enforce the inputs and outputs required by the transaction. - // NOTE: The input is provided from the action since it is only available on outputs with fungible tokens. - inputs: [], - outputs: [ - { - output: 'transferFungibleTokensOutput', - outputIndex: undefined, - }, - ], - - // Standard transaction without a locktime. - version: 2, - locktime: 0, - - // ... - composable: true, - }, - transferNonfungibleTokensTransaction: { - name: 'Non-fungible Token Transferred', + 'Received one non-fungible $( <0x02> OP_EQUAL OP_IF <"minting"> OP_ELSE <0x01> OP_EQUAL OP_IF <"mutable"> OP_ELSE <"immutable"> OP_ENDIF OP_ENDIF) () token, with $() commitment.', + icon: "receive", + }, + sender: { + name: "Sent", description: - 'One non-fungible $( <0x02> OP_EQUAL OP_IF <"minting"> OP_ELSE <0x01> OP_EQUAL OP_IF <"mutable"> OP_ELSE <"immutable"> OP_ENDIF OP_ENDIF) $() token was transferred to a recipient, with $() commitment.', - icon: 'send', - - roles: { - receiver: { - name: 'Received', - description: - 'Received one non-fungible $( <0x02> OP_EQUAL OP_IF <"minting"> OP_ELSE <0x01> OP_EQUAL OP_IF <"mutable"> OP_ELSE <"immutable"> OP_ENDIF OP_ENDIF) () token, with $() commitment.', - icon: 'receive', - }, - sender: { - name: 'Sent', - description: - 'Sent one non-fungible $( <0x02> OP_EQUAL OP_IF <"minting"> OP_ELSE <0x01> OP_EQUAL OP_IF <"mutable"> OP_ELSE <"immutable"> OP_ENDIF OP_ENDIF) () token, with $() commitment.', - icon: 'send', - }, - }, - - // Enforce the inputs and outputs required by the transaction. - // NOTE: The input is provided from the action since it is only available on outputs with a non-fungible token. - inputs: [], - outputs: [ - { - output: 'transferNonfungibleTokenOutput', - outputIndex: undefined, - }, - ], - - // Standard transaction without a locktime. - version: 2, - locktime: 0, - - // ... - composable: true, + 'Sent the requested non-fungible $( <0x02> OP_EQUAL OP_IF <"minting"> OP_ELSE <0x01> OP_EQUAL OP_IF <"mutable"> OP_ELSE <"immutable"> OP_ENDIF OP_ENDIF) () token, with $() commitment.', + icon: "send", + }, }, - burnFungibleTokensTransaction: { - name: 'Deleted fungible tokens', + // Inputs and outputs that must exist in the transaction. + // NOTE: There is no inputs required, but the engine should detect that there is not sufficient input value to + // match the output and thus generate an invitation to participate in this action. + // When the invitation is shared, the other party can add as many inputs and outputs as needed since this transaction is composable. + inputs: [], + outputs: [ + { + output: "requestNonfungibleTokensOutput", + outputIndex: undefined, + }, + ], + + // Standard transaction without a locktime. + version: 2, + locktime: 0, + + // ... + composable: true, + }, + + transferSatoshisTransaction: { + name: "Satoshis Transferred", + description: + "$() satoshis were transferred to a recipient.", + icon: "send", + + roles: { + receiver: { + name: "Received", + description: "Received $() satoshis.", + icon: "receive", + }, + sender: { + name: "Sent", + description: "Sent $() satoshis.", + icon: "send", + }, + }, + + // Enforce the inputs and outputs required by the transaction. + // NOTE: The input is provided from the action since it is only available on outputs with satoshis. + inputs: [], + outputs: [ + { + output: "transferSatoshisOutput", + outputIndex: undefined, + }, + ], + + // Standard transaction without a locktime. + version: 2, + locktime: 0, + + // ... + composable: true, + }, + transferFungibleTokensTransaction: { + name: "Fungible Tokens Transferred", + description: + "$( OP_DIV).$( OP_MOD) $() tokens were transferred to a recipient.", + icon: "send", + + roles: { + receiver: { + name: "Received", description: - 'Permanently and irreversibly deleted $( OP_DIV).$( OP_MOD) $() tokens.', - icon: 'burn', - - // Inputs and outputs that must exist in the transaction. - // NOTE: There is no defined outputs as any non-burned value is automatically returned as change. - inputs: [ - { - input: 'burnFungibleTokensInput', - inputIndex: undefined, - }, - ], - - outputs: [], - - // Standard transaction without a locktime. - version: 2, - locktime: 0, - - // ... - composable: true, - }, - burnNonfungibleTokenTransaction: { - name: 'Deleted non fungible token', + "Received $( OP_DIV).$( OP_MOD) $() tokens.", + icon: "receive", + }, + sender: { + name: "Sent", description: - 'Permanently and irreversibly deleted a non-fungible $( <0x02> OP_EQUAL OP_IF <"minting"> OP_ELSE <0x01> OP_EQUAL OP_IF <"mutable"> OP_ELSE <"immutable"> OP_ENDIF OP_ENDIF) () token, with $() commitment.', - icon: 'burn', - - // Inputs and outputs that must exist in the transaction. - // NOTE: There is no defined outputs as any non-burned value is automatically returned as change. - inputs: [ - { - input: 'burnNonfungibleTokenInput', - inputIndex: undefined, - }, - ], - outputs: [], - - // Standard transaction without a locktime. - version: 2, - locktime: 0, - - // ... - composable: true, + "Sent $( OP_DIV).$( OP_MOD) $() tokens.", + icon: "send", + }, }, + + // Enforce the inputs and outputs required by the transaction. + // NOTE: The input is provided from the action since it is only available on outputs with fungible tokens. + inputs: [], + outputs: [ + { + output: "transferFungibleTokensOutput", + outputIndex: undefined, + }, + ], + + // Standard transaction without a locktime. + version: 2, + locktime: 0, + + // ... + composable: true, + }, + transferNonfungibleTokensTransaction: { + name: "Non-fungible Token Transferred", + description: + 'One non-fungible $( <0x02> OP_EQUAL OP_IF <"minting"> OP_ELSE <0x01> OP_EQUAL OP_IF <"mutable"> OP_ELSE <"immutable"> OP_ENDIF OP_ENDIF) $() token was transferred to a recipient, with $() commitment.', + icon: "send", + + roles: { + receiver: { + name: "Received", + description: + 'Received one non-fungible $( <0x02> OP_EQUAL OP_IF <"minting"> OP_ELSE <0x01> OP_EQUAL OP_IF <"mutable"> OP_ELSE <"immutable"> OP_ENDIF OP_ENDIF) () token, with $() commitment.', + icon: "receive", + }, + sender: { + name: "Sent", + description: + 'Sent one non-fungible $( <0x02> OP_EQUAL OP_IF <"minting"> OP_ELSE <0x01> OP_EQUAL OP_IF <"mutable"> OP_ELSE <"immutable"> OP_ENDIF OP_ENDIF) () token, with $() commitment.', + icon: "send", + }, + }, + + // Enforce the inputs and outputs required by the transaction. + // NOTE: The input is provided from the action since it is only available on outputs with a non-fungible token. + inputs: [], + outputs: [ + { + output: "transferNonfungibleTokenOutput", + outputIndex: undefined, + }, + ], + + // Standard transaction without a locktime. + version: 2, + locktime: 0, + + // ... + composable: true, + }, + + burnFungibleTokensTransaction: { + name: "Deleted fungible tokens", + description: + "Permanently and irreversibly deleted $( OP_DIV).$( OP_MOD) $() tokens.", + icon: "burn", + + // Inputs and outputs that must exist in the transaction. + // NOTE: There is no defined outputs as any non-burned value is automatically returned as change. + inputs: [ + { + input: "burnFungibleTokensInput", + inputIndex: undefined, + }, + ], + + outputs: [], + + // Standard transaction without a locktime. + version: 2, + locktime: 0, + + // ... + composable: true, + }, + burnNonfungibleTokenTransaction: { + name: "Deleted non fungible token", + description: + 'Permanently and irreversibly deleted a non-fungible $( <0x02> OP_EQUAL OP_IF <"minting"> OP_ELSE <0x01> OP_EQUAL OP_IF <"mutable"> OP_ELSE <"immutable"> OP_ENDIF OP_ENDIF) () token, with $() commitment.', + icon: "burn", + + // Inputs and outputs that must exist in the transaction. + // NOTE: There is no defined outputs as any non-burned value is automatically returned as change. + inputs: [ + { + input: "burnNonfungibleTokenInput", + inputIndex: undefined, + }, + ], + outputs: [], + + // Standard transaction without a locktime. + version: 2, + locktime: 0, + + // ... + composable: true, + }, }, // Define a set of outputs that can be used within transactions in this template. outputs: { - changeOutput: { - name: 'Change', - description: 'Funds returned as change.', - icon: 'receive', + changeOutput: { + name: "Change", + description: "Funds returned as change.", + icon: "receive", - // Defines how the requested funds should be locked. - lockingScript: 'receivingLockingScript', + // Defines how the requested funds should be locked. + lockingScript: "receivingLockingScript", + }, + receiveOutput: { + name: "Recipient output", + description: + "Transferred an unspecified amount of cash and/or tokens to a recipient.", + icon: "receive", + + roles: { + receiver: { + name: "Received", + description: "Received an unspecified amount of cash and/or tokens.", + }, + sender: { + name: "Sent", + description: "Sent an unspecified amount of cash and/or tokens.", + }, }, - receiveOutput: { - name: 'Recipient output', - description: 'Transferred an unspecified amount of cash and/or tokens to a recipient.', - icon: 'receive', - roles: { - receiver: { - name: 'Received', - description: 'Received an unspecified amount of cash and/or tokens.', - }, - sender: { - name: 'Sent', - description: 'Sent an unspecified amount of cash and/or tokens.', - }, - }, + // Defines how the requested funds should be locked. + lockingScript: "receivingLockingScript", + }, + requestSatoshisOutput: { + name: "Satoshis", + description: "$() satoshis.", + icon: "request", - // Defines how the requested funds should be locked. - lockingScript: 'receivingLockingScript', + roles: { + receiver: { + name: "Satoshis Received", + description: "Received $() satoshis.", + }, + sender: { + name: "Satoshis Sent", + description: "Sent $() satoshis.", + }, }, - requestSatoshisOutput: { - name: 'Satoshis', - description: '$() satoshis.', - icon: 'request', - roles: { - receiver: { - name: 'Satoshis Received', - description: 'Received $() satoshis.', - }, - sender: { - name: 'Satoshis Sent', - description: 'Sent $() satoshis.', - }, - }, + // Defines how the requested funds should be locked. + lockingScript: "receivingLockingScript", - // Defines how the requested funds should be locked. - lockingScript: 'receivingLockingScript', + // Require the specified number of satoshis and no tokens. + valueSatoshis: "$()", + token: null, + }, + requestFungibleTokensOutput: { + name: "Fungible $() Tokens", + description: + "$( OP_DIV).$( OP_MOD) $() tokens.", + icon: "request", - // Require the specified number of satoshis and no tokens. - valueSatoshis: '$()', - token: null, - }, - requestFungibleTokensOutput: { - name: 'Fungible $() Tokens', + roles: { + receiver: { + name: "Fungible $() Tokens Received", description: - '$( OP_DIV).$( OP_MOD) $() tokens.', - icon: 'request', - - roles: { - receiver: { - name: 'Fungible $() Tokens Received', - description: - 'Received $( OP_DIV).$( OP_MOD) $() tokens.', - }, - sender: { - name: 'Fungible $() Tokens Sent', - description: - 'Sent $( OP_DIV).$( OP_MOD) $() tokens.', - }, - }, - - // Defines how the requested funds should be locked. - lockingScript: 'receivingLockingScript', - - // Require a flat 1000 satoshis to ensure the fungible tokens remains transferrable. - valueSatoshis: '1000', - - // Require only the specified amount and type of fungible tokens. - // NOTE: This can be composed with a request for a non-fungible token, but will always result in two separate outputs. - token: { - category: '$()', - amount: '$()', - nft: null, - }, - }, - requestNonfungibleTokensOutput: { - name: 'Non-fungible $() Token', + "Received $( OP_DIV).$( OP_MOD) $() tokens.", + }, + sender: { + name: "Fungible $() Tokens Sent", description: - 'Transferred one non-fungible $( <0x02> OP_EQUAL OP_IF <"minting"> OP_ELSE <0x01> OP_EQUAL OP_IF <"mutable"> OP_ELSE <"immutable"> OP_ENDIF OP_ENDIF) $() token to a recipient, with $() commitment.', - icon: 'request', - - roles: { - receiver: { - name: 'Non-fungible $() Token Received', - description: - 'Received one non-fungible $( <0x02> OP_EQUAL OP_IF <"minting"> OP_ELSE <0x01> OP_EQUAL OP_IF <"mutable"> OP_ELSE <"immutable"> OP_ENDIF OP_ENDIF) $() token, with $() commitment.', - }, - sender: { - name: 'Non-fungible $() Token Sent', - description: - 'Sent the requested non-fungible $( <0x02> OP_EQUAL OP_IF <"minting"> OP_ELSE <0x01> OP_EQUAL OP_IF <"mutable"> OP_ELSE <"immutable"> OP_ENDIF OP_ENDIF) $() token, with $() commitment.', - }, - }, - - // Defines how the requested funds should be locked. - lockingScript: 'receivingLockingScript', - - // Require a flat 1000 satoshis to ensure the non-fungible token remains transferrable. - valueSatoshis: '1000', - - // Require only an NFT with specified category, capability and commitment. - // NOTE: This can be composed with a request for fungible token amounts, but will always result in two separate outputs. - token: { - category: '$()', - amount: null, - nft: { - capability: '$()', - commitment: '$()', - }, - }, + "Sent $( OP_DIV).$( OP_MOD) $() tokens.", + }, }, - transferSatoshisOutput: { - name: 'Recipient output', - description: 'Transferred $() satoshis to a recipient.', - icon: 'send', + // Defines how the requested funds should be locked. + lockingScript: "receivingLockingScript", - roles: { - receiver: { - name: 'Received', - description: 'Received $() satoshis.', - }, - sender: { - name: 'Sent', - description: 'Sent $() satoshis.', - }, - }, + // Require a flat 1000 satoshis to ensure the fungible tokens remains transferrable. + valueSatoshis: "1000", - // Use the recipients lockscript. - lockingScript: 'sendingLockingscript', - - // Set the amount of satoshis to transfer. - valueSatoshis: '$()', + // Require only the specified amount and type of fungible tokens. + // NOTE: This can be composed with a request for a non-fungible token, but will always result in two separate outputs. + token: { + category: "$()", + amount: "$()", + nft: null, }, - transferFungibleTokensOutput: { - name: 'Recipient output', + }, + requestNonfungibleTokensOutput: { + name: "Non-fungible $() Token", + description: + 'Transferred one non-fungible $( <0x02> OP_EQUAL OP_IF <"minting"> OP_ELSE <0x01> OP_EQUAL OP_IF <"mutable"> OP_ELSE <"immutable"> OP_ENDIF OP_ENDIF) $() token to a recipient, with $() commitment.', + icon: "request", + + roles: { + receiver: { + name: "Non-fungible $() Token Received", description: - 'Transferred $( OP_DIV).$( OP_MOD) $() tokens to a recipient.', - icon: 'send', - - roles: { - receiver: { - name: 'Received', - description: - 'Received $( OP_DIV).$( OP_MOD) $() tokens.', - }, - sender: { - name: 'Sent', - description: - 'Sent $( OP_DIV).$( OP_MOD) $() tokens.', - }, - }, - - // Use the recipients lockscript. - lockingScript: 'sendingLockingscript', - - // Set the amount of fungible tokens to transfer. - token: { - category: '$()', - amount: '$()', - }, - }, - transferNonfungibleTokenOutput: { - name: 'Recipient output', + 'Received one non-fungible $( <0x02> OP_EQUAL OP_IF <"minting"> OP_ELSE <0x01> OP_EQUAL OP_IF <"mutable"> OP_ELSE <"immutable"> OP_ENDIF OP_ENDIF) $() token, with $() commitment.', + }, + sender: { + name: "Non-fungible $() Token Sent", description: - 'Transferred one non-fungible $( <0x02> OP_EQUAL OP_IF <"minting"> OP_ELSE <0x01> OP_EQUAL OP_IF <"mutable"> OP_ELSE <"immutable"> OP_ENDIF OP_ENDIF) $() token to a recipient, with $() commitment.', - icon: 'send', - - roles: { - receiver: { - name: 'Received', - description: - 'Received one non-fungible $( <0x02> OP_EQUAL OP_IF <"minting"> OP_ELSE <0x01> OP_EQUAL OP_IF <"mutable"> OP_ELSE <"immutable"> OP_ENDIF OP_ENDIF) $() token, with $() commitment.', - }, - sender: { - name: 'Sent', - description: - 'Sent one non-fungible $( <0x02> OP_EQUAL OP_IF <"minting"> OP_ELSE <0x01> OP_EQUAL OP_IF <"mutable"> OP_ELSE <"immutable"> OP_ENDIF OP_ENDIF) $() token, with $() commitment.', - }, - }, - - // Use the recipients lockscript. - lockingScript: 'sendingLockingscript', - - // Set the non-fungible token to transfer. - token: { - category: '$()', - nft: { - capability: '$()', - commitment: '$()', - }, - }, + 'Sent the requested non-fungible $( <0x02> OP_EQUAL OP_IF <"minting"> OP_ELSE <0x01> OP_EQUAL OP_IF <"mutable"> OP_ELSE <"immutable"> OP_ENDIF OP_ENDIF) $() token, with $() commitment.', + }, }, + + // Defines how the requested funds should be locked. + lockingScript: "receivingLockingScript", + + // Require a flat 1000 satoshis to ensure the non-fungible token remains transferrable. + valueSatoshis: "1000", + + // Require only an NFT with specified category, capability and commitment. + // NOTE: This can be composed with a request for fungible token amounts, but will always result in two separate outputs. + token: { + category: "$()", + amount: null, + nft: { + capability: "$()", + commitment: "$()", + }, + }, + }, + + transferSatoshisOutput: { + name: "Recipient output", + description: + "Transferred $() satoshis to a recipient.", + icon: "send", + + roles: { + receiver: { + name: "Received", + description: "Received $() satoshis.", + }, + sender: { + name: "Sent", + description: "Sent $() satoshis.", + }, + }, + + // Use the recipients lockscript. + lockingScript: "sendingLockingscript", + + // Set the amount of satoshis to transfer. + valueSatoshis: "$()", + }, + transferFungibleTokensOutput: { + name: "Recipient output", + description: + "Transferred $( OP_DIV).$( OP_MOD) $() tokens to a recipient.", + icon: "send", + + roles: { + receiver: { + name: "Received", + description: + "Received $( OP_DIV).$( OP_MOD) $() tokens.", + }, + sender: { + name: "Sent", + description: + "Sent $( OP_DIV).$( OP_MOD) $() tokens.", + }, + }, + + // Use the recipients lockscript. + lockingScript: "sendingLockingscript", + + // Set the amount of fungible tokens to transfer. + token: { + category: "$()", + amount: "$()", + }, + }, + transferNonfungibleTokenOutput: { + name: "Recipient output", + description: + 'Transferred one non-fungible $( <0x02> OP_EQUAL OP_IF <"minting"> OP_ELSE <0x01> OP_EQUAL OP_IF <"mutable"> OP_ELSE <"immutable"> OP_ENDIF OP_ENDIF) $() token to a recipient, with $() commitment.', + icon: "send", + + roles: { + receiver: { + name: "Received", + description: + 'Received one non-fungible $( <0x02> OP_EQUAL OP_IF <"minting"> OP_ELSE <0x01> OP_EQUAL OP_IF <"mutable"> OP_ELSE <"immutable"> OP_ENDIF OP_ENDIF) $() token, with $() commitment.', + }, + sender: { + name: "Sent", + description: + 'Sent one non-fungible $( <0x02> OP_EQUAL OP_IF <"minting"> OP_ELSE <0x01> OP_EQUAL OP_IF <"mutable"> OP_ELSE <"immutable"> OP_ENDIF OP_ENDIF) $() token, with $() commitment.', + }, + }, + + // Use the recipients lockscript. + lockingScript: "sendingLockingscript", + + // Set the non-fungible token to transfer. + token: { + category: "$()", + nft: { + capability: "$()", + commitment: "$()", + }, + }, + }, }, inputs: { - burnFungibleTokensInput: { - name: 'Deleted fungible tokens', - description: 'Permanently and irreversibly deleted $() $().', - icon: 'burn', + burnFungibleTokensInput: { + name: "Deleted fungible tokens", + description: + "Permanently and irreversibly deleted $() $().", + icon: "burn", - // Define which unlocking script unlocks this input. - unlockingScript: 'unlockP2PKH', + // Define which unlocking script unlocks this input. + unlockingScript: "unlockP2PKH", - // Require a fungible token of the requested token category, with an amount larger than or equal to the requested amount to burn. - token: { - category: '$()', - amount: '$( OP_GREATERTHANOREQUAL OP_IF OP_ENDIF)', - }, - - // Ignore the burned token amount when determining change for this output. - // NOTE: The engine must check that this does not exceed the input value. - omitChangeAmounts: { - fungibleTokens: '${}', - }, + // Require a fungible token of the requested token category, with an amount larger than or equal to the requested amount to burn. + token: { + category: "$()", + amount: + "$( OP_GREATERTHANOREQUAL OP_IF OP_ENDIF)", }, - burnNonfungibleTokenInput: { - name: 'Deleted non-fungible token', - description: - 'Permanently and irreversibly burned one non-fungible $( <0x02> OP_EQUAL OP_IF <"minting"> OP_ELSE <0x01> OP_EQUAL OP_IF <"mutable"> OP_ELSE <"immutable"> OP_ENDIF OP_ENDIF) token of category $(), with a $() commitment.', - icon: 'burn', - // Define which unlocking script unlocks this input. - unlockingScript: 'unlockP2PKH', - - // Require a non-fungible token of the specified category, capability and commitment to burn. - token: { - category: '$()', - nft: { - capability: '$()', - commitment: '$()', - }, - }, - - // Ignore the burned token when determining change for this output. - // NOTE: The engine must check that this does not exceed the input value. - omitChangeAmounts: { - nonfungibleTokens: 1, - }, + // Ignore the burned token amount when determining change for this output. + // NOTE: The engine must check that this does not exceed the input value. + omitChangeAmounts: { + fungibleTokens: "${}", }, + }, + burnNonfungibleTokenInput: { + name: "Deleted non-fungible token", + description: + 'Permanently and irreversibly burned one non-fungible $( <0x02> OP_EQUAL OP_IF <"minting"> OP_ELSE <0x01> OP_EQUAL OP_IF <"mutable"> OP_ELSE <"immutable"> OP_ENDIF OP_ENDIF) token of category $(), with a $() commitment.', + icon: "burn", + + // Define which unlocking script unlocks this input. + unlockingScript: "unlockP2PKH", + + // Require a non-fungible token of the specified category, capability and commitment to burn. + token: { + category: "$()", + nft: { + capability: "$()", + commitment: "$()", + }, + }, + + // Ignore the burned token when determining change for this output. + // NOTE: The engine must check that this does not exceed the input value. + omitChangeAmounts: { + nonfungibleTokens: 1, + }, + }, }, // Define locking scripts used by this template. // NOTE: Template supported wallets should automatically track all generated lockscripts for on-chain events. lockingScripts: { - sendingLockingscript: { - // TODO: This currently describes outputs locked to this script, but all actions creating such outputs already have descriptions. - // This can either be dropped, or maybe more importantly, should be disambiguated so that lockscripts can provide separate - // descriptions for their script and the outputs that are locked to them. Leaving as a TODO for now and will address later. - name: 'Sent', - description: 'Funds sent to an external recipient', - icon: 'address', + sendingLockingscript: { + // TODO: This currently describes outputs locked to this script, but all actions creating such outputs already have descriptions. + // This can either be dropped, or maybe more importantly, should be disambiguated so that lockscripts can provide separate + // descriptions for their script and the outputs that are locked to them. Leaving as a TODO for now and will address later. + name: "Sent", + description: "Funds sent to an external recipient", + icon: "address", - // ... - lockingType: 'p2pkh', - lockingBytecode: 'lockToRecipient', + // ... + lockingType: "p2pkh", + lockingBytecode: "lockToRecipient", - // Indicate that the sent output does not belong to the initiating user. - // NOTE: These values default to false/empty, but added here for additional clarity. - actions: [], - state: { variables: [], secrets: [] }, - balance: {}, - selectable: false, - }, - receivingLockingScript: { - // NOTE: Outputs to this lockscript by external actors defaults to this description when detected on-chain. - name: 'Received', - description: 'Funds received without wallet coordination.', - icon: 'address', + // Indicate that the sent output does not belong to the initiating user. + // NOTE: These values default to false/empty, but added here for additional clarity. + actions: [], + state: { variables: [], secrets: [] }, + balance: {}, + selectable: false, + }, + receivingLockingScript: { + // NOTE: Outputs to this lockscript by external actors defaults to this description when detected on-chain. + name: "Received", + description: "Funds received without wallet coordination.", + icon: "address", - // Defines how spending future received funds should be locked. - lockingType: 'p2pkh', - lockingBytecode: 'lockP2PKH', + // Defines how spending future received funds should be locked. + lockingType: "p2pkh", + lockingBytecode: "lockP2PKH", - // Define a default unlocking script to be used when no action provided script is present. - unlockingBytecode: 'unlockP2PKH', + // Define a default unlocking script to be used when no action provided script is present. + unlockingBytecode: "unlockP2PKH", - // Participants without a role or observers cannot take any further actions. - // NOTE: This is not required but shown here for illustrative purposes. - actions: [], + // Participants without a role or observers cannot take any further actions. + // NOTE: This is not required but shown here for illustrative purposes. + actions: [], - roles: { - receiver: { - // The only state that is required to be persisted when receiving funds is the owners private key. - // NOTE: This is defined as a secret to not leak when creating invitations for others to participate in request or send actions. - state: { - variables: [], - secrets: [ 'ownerKey' ], - }, - - // List actions that can be taken with the ownerKey for each address/lockscript. - actions: [ - { - action: 'sign', - role: 'owner', - secrets: [{ ownerKey: null }], - }, - { - action: 'verify', - role: 'owner', - secrets: [{ ownerKey: null }], - }, - { - action: 'sendSatoshis', - role: 'sender', - secrets: [{ ownerKey: null }], - }, - { - action: 'sendFungibleTokens', - role: 'sender', - secrets: [{ ownerKey: null }], - }, - { - action: 'sendNonfungibleTokens', - role: 'sender', - secrets: [{ ownerKey: null }], - }, - { - action: 'burnFungibleTokens', - role: 'sender', - secrets: [{ ownerKey: null }], - }, - { - action: 'burnNonfungibleTokens', - role: 'sender', - secrets: [{ ownerKey: null }], - }, - ], - - // Indicates how much of received funds should be part of a wallets total balance. - // NOTE: Evaluates in the context of the source transaction, with the current outputs technical data available under this.output.* - // NOTE: A CashASM expression of '1' means include 100% of this asset in the balance. - // NOTE: Since we know that the owner controls all assets, we short-cut the evaluations by setting 1 directly. - balance: { - satoshis: true, - fungibleTokens: true, - nonfungibleTokens: true, - }, - - // Indicate when received funds should be considered in automatic coin selection to meet input requirements of other transactions. - // NOTE: Evaluates in the context of the source transaction, with the current outputs technical data available under this.output.* - // NOTE: This evaluates to a boolean. If non-true, the output should not be considered for automatic coin selection. - // NOTE: Since we know that the secret key exist for the owner, this template short-cuts the evaluation by setting true directly. - selectable: true, - }, + roles: { + receiver: { + // The only state that is required to be persisted when receiving funds is the owners private key. + // NOTE: This is defined as a secret to not leak when creating invitations for others to participate in request or send actions. + state: { + variables: [], + secrets: ["ownerKey"], }, + + // List actions that can be taken with the ownerKey for each address/lockscript. + actions: [ + { + action: "sign", + role: "owner", + secrets: [{ ownerKey: null }], + }, + { + action: "verify", + role: "owner", + secrets: [{ ownerKey: null }], + }, + { + action: "sendSatoshis", + role: "sender", + secrets: [{ ownerKey: null }], + }, + { + action: "sendFungibleTokens", + role: "sender", + secrets: [{ ownerKey: null }], + }, + { + action: "sendNonfungibleTokens", + role: "sender", + secrets: [{ ownerKey: null }], + }, + { + action: "burnFungibleTokens", + role: "sender", + secrets: [{ ownerKey: null }], + }, + { + action: "burnNonfungibleTokens", + role: "sender", + secrets: [{ ownerKey: null }], + }, + ], + + // Indicates how much of received funds should be part of a wallets total balance. + // NOTE: Evaluates in the context of the source transaction, with the current outputs technical data available under this.output.* + // NOTE: A CashASM expression of '1' means include 100% of this asset in the balance. + // NOTE: Since we know that the owner controls all assets, we short-cut the evaluations by setting 1 directly. + balance: { + satoshis: true, + fungibleTokens: true, + nonfungibleTokens: true, + }, + + // Indicate when received funds should be considered in automatic coin selection to meet input requirements of other transactions. + // NOTE: Evaluates in the context of the source transaction, with the current outputs technical data available under this.output.* + // NOTE: This evaluates to a boolean. If non-true, the output should not be considered for automatic coin selection. + // NOTE: Since we know that the secret key exist for the owner, this template short-cuts the evaluation by setting true directly. + selectable: true, + }, }, + }, }, // Define a set of scripts that can be used in this template. scripts: { - lockP2PKH: 'OP_DUP OP_HASH160 <$( OP_HASH160)> OP_EQUALVERIFY OP_CHECKSIG', - unlockP2PKH: ' ', - lockToRecipient: '', + lockP2PKH: + "OP_DUP OP_HASH160 <$( OP_HASH160)> OP_EQUALVERIFY OP_CHECKSIG", + unlockP2PKH: + " ", + lockToRecipient: "", }, // TODO: Add icons constants: { - dustLimit: { - name: 'Dust Limit', - description: 'Standard required minimum satoshis for Pay to Public Key Hash outputs.', - type: 'integer', - value: 546, - }, + dustLimit: { + name: "Dust Limit", + description: + "Standard required minimum satoshis for Pay to Public Key Hash outputs.", + type: "integer", + value: 546, + }, - // Define a message prefix for use in arbitrary message signing. - messagePrefix: { - name: 'Message Prefix', - description: 'Standard message prefix used in the bitcoin signed message protocol.', - type: 'bytes', + // Define a message prefix for use in arbitrary message signing. + messagePrefix: { + name: "Message Prefix", + description: + "Standard message prefix used in the bitcoin signed message protocol.", + type: "bytes", - // Value is enforced to the bitcoin signing magic string: - // "\x18Bitcoin Signed Message:\n" - value: '0x18426974636f696e205369676e6564204d6573736167653a0a', - }, + // Value is enforced to the bitcoin signing magic string: + // "\x18Bitcoin Signed Message:\n" + value: "0x18426974636f696e205369676e6564204d6573736167653a0a", + }, }, // TODO: Add icons variables: { - // Describe the secret private key. - ownerKey: { - name: 'Owners Private Key', - description: 'The private key used to authorize spending of received funds.', - type: 'bytes', - hint: 'private_key', - }, + // Describe the secret private key. + ownerKey: { + name: "Owners Private Key", + description: + "The private key used to authorize spending of received funds.", + type: "bytes", + hint: "private_key", + }, - messageToSign: { - name: 'Message', - description: 'The text message to sign.', - type: 'string', - }, - messageToVerify: { - name: 'Message', - description: 'The text message to verify.', - type: 'string', - }, - messageSignature: { - name: 'Message Signature', - description: 'The signature for the message.', - type: 'bytes', - hint: 'signature', - }, + messageToSign: { + name: "Message", + description: "The text message to sign.", + type: "string", + }, + messageToVerify: { + name: "Message", + description: "The text message to verify.", + type: "string", + }, + messageSignature: { + name: "Message Signature", + description: "The signature for the message.", + type: "bytes", + hint: "signature", + }, - // Describe the parameters used when requesting value. - requestedSatoshis: { - name: 'Requested Amount', - description: 'The Bitcoin Cash amount requested', - type: 'integer', - hint: 'satoshis', - }, - requestedTokenCategory: { - name: 'Requested Token Category', - description: 'The token category requested', - type: 'bytes', - hint: 'token_category', - }, - requestedTokenAmount: { - name: 'Requested Token Amount', - description: 'The fungible token amount requested', - type: 'integer', - hint: 'token_amount', - }, - requestedTokenCapability: { - name: 'Requested Token Capability', - description: 'The non-fungible token capability requested', - type: 'bytes', - hint: 'token_capability', - }, - requestedTokenCommitment: { - name: 'Requested Token Commitment', - description: 'The non-fungible token commitment requested', - type: 'bytes', - hint: 'token_commitment', - }, - transferredTokenCategory: { - name: 'Sending Token Category', - description: 'The token category of the token(s) to send', - type: 'bytes', - hint: 'token_category', - }, - transferredTokenAmount: { - name: 'Sending Token Amount', - description: 'The fungible token amount to send', - type: 'integer', - hint: 'token_amount', - }, - transferredTokenCapability: { - name: 'Sending Token Capability', - description: 'The token capability for the non-fungible token to send', - type: 'bytes', - hint: 'token_capability', - }, - transferredTokenCommitment: { - name: 'Sending Token Commitment', - description: 'The token commitment for the non-fungible token to send', - type: 'bytes', - hint: 'token_commitment', - }, - burnedTokenCategory: { - name: 'Deleted Token Category', - description: 'The token category of the token(s) to delete', - type: 'bytes', - hint: 'token_category', - }, - burnedTokenAmount: { - name: 'Deleted Token Amount', - description: 'The fungible token amount to delete', - type: 'integer', - hint: 'token_amount', - }, - burnedTokenCapability: { - name: 'Deleted Token Capability', - description: 'The token capability for the non-fungible token to delete', - type: 'bytes', - hint: 'token_capability', - }, - burnedTokenCommitment: { - name: 'Deleted Token Commitment', - description: 'The token commitment for the non-fungible token to delete', - type: 'bytes', - hint: 'token_commitment', - }, + // Describe the parameters used when requesting value. + requestedSatoshis: { + name: "Requested Amount", + description: "The Bitcoin Cash amount requested", + type: "integer", + hint: "satoshis", + }, + requestedTokenCategory: { + name: "Requested Token Category", + description: "The token category requested", + type: "bytes", + hint: "token_category", + }, + requestedTokenAmount: { + name: "Requested Token Amount", + description: "The fungible token amount requested", + type: "integer", + hint: "token_amount", + }, + requestedTokenCapability: { + name: "Requested Token Capability", + description: "The non-fungible token capability requested", + type: "bytes", + hint: "token_capability", + }, + requestedTokenCommitment: { + name: "Requested Token Commitment", + description: "The non-fungible token commitment requested", + type: "bytes", + hint: "token_commitment", + }, + transferredTokenCategory: { + name: "Sending Token Category", + description: "The token category of the token(s) to send", + type: "bytes", + hint: "token_category", + }, + transferredTokenAmount: { + name: "Sending Token Amount", + description: "The fungible token amount to send", + type: "integer", + hint: "token_amount", + }, + transferredTokenCapability: { + name: "Sending Token Capability", + description: "The token capability for the non-fungible token to send", + type: "bytes", + hint: "token_capability", + }, + transferredTokenCommitment: { + name: "Sending Token Commitment", + description: "The token commitment for the non-fungible token to send", + type: "bytes", + hint: "token_commitment", + }, + burnedTokenCategory: { + name: "Deleted Token Category", + description: "The token category of the token(s) to delete", + type: "bytes", + hint: "token_category", + }, + burnedTokenAmount: { + name: "Deleted Token Amount", + description: "The fungible token amount to delete", + type: "integer", + hint: "token_amount", + }, + burnedTokenCapability: { + name: "Deleted Token Capability", + description: "The token capability for the non-fungible token to delete", + type: "bytes", + hint: "token_capability", + }, + burnedTokenCommitment: { + name: "Deleted Token Commitment", + description: "The token commitment for the non-fungible token to delete", + type: "bytes", + hint: "token_commitment", + }, }, // Define a list of re-usable icons that can be used as part of metadata. // NOTE: the actual icons are not embedded in the template but only referenced by hashes here, // and can be distributed either as an asset pack or looked up through something like IPFS. icons: [ - { - name: 'wallet', - hash: '0000000000000000000000', - }, - { - name: 'owner', - hash: '0000000000000000000000', - }, - { - name: 'sender', - hash: '0000000000000000000000', - }, - { - name: 'address', - hash: '0000000000000000000000', - }, - { - name: 'receive', - hash: '0000000000000000000000', - }, - { - name: 'request', - hash: '0000000000000000000000', - }, - { - name: 'send', - hash: '0000000000000000000000', - }, - { - name: 'burn', - hash: '0000000000000000000000', - }, - { - name: 'sign', - hash: '0000000000000000000000', - }, - { - name: 'verify', - hash: '0000000000000000000000', - }, + { + name: "wallet", + hash: "0000000000000000000000", + }, + { + name: "owner", + hash: "0000000000000000000000", + }, + { + name: "sender", + hash: "0000000000000000000000", + }, + { + name: "address", + hash: "0000000000000000000000", + }, + { + name: "receive", + hash: "0000000000000000000000", + }, + { + name: "request", + hash: "0000000000000000000000", + }, + { + name: "send", + hash: "0000000000000000000000", + }, + { + name: "burn", + hash: "0000000000000000000000", + }, + { + name: "sign", + hash: "0000000000000000000000", + }, + { + name: "verify", + hash: "0000000000000000000000", + }, ], scenarios: [ - { - name: 'requesting satoshis', - description: 'happy-path evaluation for requesting satoshis.', + { + name: "requesting satoshis", + description: "happy-path evaluation for requesting satoshis.", - // The action being run in the scenario. - action: 'requestSatoshis', + // The action being run in the scenario. + action: "requestSatoshis", - // List of roles taken in this scenario, and the resources they provided. - roles: [ - { - role: 'receiver', - values: { - generated: { - ownerKey: 'KyRQa5pEXuzVcDwnXRLpYAascjchQW5DoxVRMbj4DTxS83573mz8', - }, - variables: { - requestedSatoshis: 2000, - }, - secrets: { - // This scenario does not carry any secrets. - }, - inputs: [], - outputs: [ - { - lockingBytecode: '76a91475c715ecb74178fe87933e57e947e5e92d904b8188ac', - valueSatoshis: 2000, - }, - ], - }, - }, - { - role: 'sender', - values: { - // The sender provides no raw data to the action. - generated: {}, - variables: {}, - secrets: {}, - - // The sender does provide input and change. - inputs: [ - { - outpointTransactionHash: '4ef28553a31a266719e66ba97fee3aeecd6d1788f7ff6ab12f8ebceda49660c0', - outpointIndex: 0, - sequenceNumber: 0, - unlockingBytecode: - '41226b2be7c2890c8bbde2f79e79640e56d866843f2e822ec51c469019d13db04a422c9ee49f5eefd26fee24e91910edbbb032b90cc54c34da80a61e69b0ee3d22412103e7ab26c36a7c7f45b2c26f33c08b0fa43a633268700f47216646d4cb37ae5696', - }, - ], - outputs: [ - { - lockingBytecode: '76a91475c715ecb74178fe87933e57e947e5e92d904b8188ac', - valueSatoshis: 2000, - }, - ], - }, - }, - ], - - // List of resources provided outside the context of a role. + // List of roles taken in this scenario, and the resources they provided. + roles: [ + { + role: "receiver", values: { - generated: { - // This scenario does not have any non-role generated values. - }, - variables: { - // This scenario does not have any non-role variables. - }, - secrets: { - // This scenario does not have any non-role secrets. + generated: { + ownerKey: "KyRQa5pEXuzVcDwnXRLpYAascjchQW5DoxVRMbj4DTxS83573mz8", + }, + variables: { + requestedSatoshis: 2000, + }, + secrets: { + // This scenario does not carry any secrets. + }, + inputs: [], + outputs: [ + { + lockingBytecode: + "76a91475c715ecb74178fe87933e57e947e5e92d904b8188ac", + valueSatoshis: 2000, }, + ], }, + }, + { + role: "sender", + values: { + // The sender provides no raw data to the action. + generated: {}, + variables: {}, + secrets: {}, - // Outcomes provides the set of resulting values created from the action. - outcome: { - roles: { - receiver: { - name: 'Request', - description: 'Requested a specific amount of satoshis from one or more senders.', - icon: 'request', - }, - sender: { - name: 'Send', - description: 'Sent a specific amount of satoshis to the provided receiver.', - icon: 'send', - }, + // The sender does provide input and change. + inputs: [ + { + outpointTransactionHash: + "4ef28553a31a266719e66ba97fee3aeecd6d1788f7ff6ab12f8ebceda49660c0", + outpointIndex: 0, + sequenceNumber: 0, + unlockingBytecode: + "41226b2be7c2890c8bbde2f79e79640e56d866843f2e822ec51c469019d13db04a422c9ee49f5eefd26fee24e91910edbbb032b90cc54c34da80a61e69b0ee3d22412103e7ab26c36a7c7f45b2c26f33c08b0fa43a633268700f47216646d4cb37ae5696", }, - - transactions: [ - { - transaction: - '0200000001c06096a4edbc8e2fb16afff788176dcdee3aee7fa96be61967261aa35385f24e000000006441226b2be7c2890c8bbde2f79e79640e56d866843f2e822ec51c469019d13db04a422c9ee49f5eefd26fee24e91910edbbb032b90cc54c34da80a61e69b0ee3d22412103e7ab26c36a7c7f45b2c26f33c08b0fa43a633268700f47216646d4cb37ae5696000000000267530300000000001976a91475c715ecb74178fe87933e57e947e5e92d904b8188acd0070000000000001976a91475c715ecb74178fe87933e57e947e5e92d904b8188ac00000000', - value: '', - }, - ], + ], + outputs: [ + { + lockingBytecode: + "76a91475c715ecb74178fe87933e57e947e5e92d904b8188ac", + valueSatoshis: 2000, + }, + ], }, + }, + ], + + // List of resources provided outside the context of a role. + values: { + generated: { + // This scenario does not have any non-role generated values. + }, + variables: { + // This scenario does not have any non-role variables. + }, + secrets: { + // This scenario does not have any non-role secrets. + }, }, + + // Outcomes provides the set of resulting values created from the action. + outcome: { + roles: { + receiver: { + name: "Request", + description: + "Requested a specific amount of satoshis from one or more senders.", + icon: "request", + }, + sender: { + name: "Send", + description: + "Sent a specific amount of satoshis to the provided receiver.", + icon: "send", + }, + }, + + transactions: [ + { + transaction: + "0200000001c06096a4edbc8e2fb16afff788176dcdee3aee7fa96be61967261aa35385f24e000000006441226b2be7c2890c8bbde2f79e79640e56d866843f2e822ec51c469019d13db04a422c9ee49f5eefd26fee24e91910edbbb032b90cc54c34da80a61e69b0ee3d22412103e7ab26c36a7c7f45b2c26f33c08b0fa43a633268700f47216646d4cb37ae5696000000000267530300000000001976a91475c715ecb74178fe87933e57e947e5e92d904b8188acd0070000000000001976a91475c715ecb74178fe87933e57e947e5e92d904b8188ac00000000", + value: "", + }, + ], + }, + }, ], }; -export const p2pkhTemplateIdentifier = - generateTemplateIdentifier(parseTemplate(p2pkhTemplate)); +export const p2pkhTemplateIdentifier = generateTemplateIdentifier( + parseTemplate(p2pkhTemplate), +); diff --git a/tests/cli/paths.test.ts b/tests/cli/paths.test.ts index 848a9d7..7824bcf 100644 --- a/tests/cli/paths.test.ts +++ b/tests/cli/paths.test.ts @@ -1,5 +1,11 @@ import { expect, test, describe, beforeEach, afterEach } from "vitest"; -import { existsSync, mkdirSync, rmSync, writeFileSync, realpathSync } from "node:fs"; +import { + existsSync, + mkdirSync, + rmSync, + writeFileSync, + realpathSync, +} from "node:fs"; import { homedir, tmpdir } from "node:os"; import path from "node:path"; @@ -130,7 +136,9 @@ describe("paths utilities", () => { // Due to some weird MacOS behavior we need to use realpathSync to get the correct path // Basically, tmpDir() returns a symlink, but moving to that path puts us at `/private/${tmpDir()}` - const expectedPath = realpathSync(path.join(tempDir, "mnemonic-cwd-test")); + const expectedPath = realpathSync( + path.join(tempDir, "mnemonic-cwd-test"), + ); // Compare to the expected path expect(resolved).toBe(expectedPath); diff --git a/tests/tui/format-dialog-message.test.ts b/tests/tui/format-dialog-message.test.ts index ecb35a9..6a229c9 100644 --- a/tests/tui/format-dialog-message.test.ts +++ b/tests/tui/format-dialog-message.test.ts @@ -21,7 +21,7 @@ describe("formatDialogMessageLines", () => { test("breaks long dot-separated paths at segment boundaries", () => { const line = - "- actions.requestFungibleTokens.roles.receiver.requirements: Unrecognized key: \"generate\""; + '- actions.requestFungibleTokens.roles.receiver.requirements: Unrecognized key: "generate"'; const lines = formatDialogMessageLines(line, 56); expect(lines.length).toBeGreaterThan(1);