Formatting
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Full Installation
|
### Full Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create a new directory since we are going to be pulling in engine too
|
# Create a new directory since we are going to be pulling in engine too
|
||||||
mkdir xo-terminal && cd xo-terminal
|
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.
|
generated assignment, to use a different wallet-state directory.
|
||||||
|
|
||||||
#### Install for bash
|
#### Install for bash
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run autocomplete:install:bash
|
npm run autocomplete:install:bash
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Install for zsh
|
#### Install for zsh
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run autocomplete:install:zsh
|
npm run autocomplete:install:zsh
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Install for fish
|
#### Install for fish
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run autocomplete:install:fish
|
npm run autocomplete:install:fish
|
||||||
```
|
```
|
||||||
|
|
||||||
### Run the CLI
|
### Run the CLI
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# If globally installed (Not really usable if not globally installed)
|
# If globally installed (Not really usable if not globally installed)
|
||||||
xo-cli
|
xo-cli
|
||||||
```
|
```
|
||||||
|
|
||||||
### Run the TUI
|
### Run the TUI
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# If globally installed
|
# If globally installed
|
||||||
xo-tui
|
xo-tui
|
||||||
|
|||||||
@@ -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.
|
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/mnemonics/` | Mnemonic files (`mnemonic-*`) |
|
||||||
| `$XO_CONFIG_DIR/data/` | Engine DB (`xo-wallet.db`) and invitation storage (`xo-invitations.db`) |
|
| `$XO_CONFIG_DIR/data/` | Engine DB (`xo-wallet.db`) and invitation storage (`xo-invitations.db`) |
|
||||||
| `$XO_CONFIG_DIR/.wallet` | JSON settings (`default-mnemonic`, `currency`) |
|
| `$XO_CONFIG_DIR/.wallet` | JSON settings (`default-mnemonic`, `currency`) |
|
||||||
@@ -41,13 +41,13 @@ npx tsx src/index.ts # TUI
|
|||||||
|
|
||||||
### Environment variables
|
### Environment variables
|
||||||
|
|
||||||
| Variable | Default |
|
| Variable | Default |
|
||||||
| ------------------------- | ----------------------------------------- |
|
| ------------------------- | --------------------------------------- |
|
||||||
| `XO_CONFIG_DIR` | `~/.config/xo-cli` |
|
| `XO_CONFIG_DIR` | `~/.config/xo-cli` |
|
||||||
| `SYNC_SERVER_URL` | `http://localhost:3000` |
|
| `SYNC_SERVER_URL` | `http://localhost:3000` |
|
||||||
| `DB_PATH` | `$XO_CONFIG_DIR/data` |
|
| `DB_PATH` | `$XO_CONFIG_DIR/data` |
|
||||||
| `DB_FILENAME` | `xo-wallet.db` |
|
| `DB_FILENAME` | `xo-wallet.db` |
|
||||||
| `INVITATION_STORAGE_PATH` | `$XO_CONFIG_DIR/data/xo-invitations.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.
|
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`)
|
## Global Options (`xo-cli`)
|
||||||
|
|
||||||
| Flag | Description |
|
| Flag | Description |
|
||||||
| ------------------------------ | --------------------------------------------------- |
|
| ------------------------------ | ---------------------------------------------------- |
|
||||||
| `-m`, `--mnemonic-file <file>` | Mnemonic file (basename, cwd-relative, or absolute) |
|
| `-m`, `--mnemonic-file <file>` | Mnemonic file (basename, cwd-relative, or absolute) |
|
||||||
| `--currency <code>` | Fiat display currency (e.g. `USD`, `AUD`) |
|
| `--currency <code>` | Fiat display currency (e.g. `USD`, `AUD`) |
|
||||||
| `-o`, `--output <filename>` | Output filename (used by `mnemonic create`/`import`) |
|
| `-o`, `--output <filename>` | Output filename (used by `mnemonic create`/`import`) |
|
||||||
| `-v`, `--verbose` | Verbose output |
|
| `-v`, `--verbose` | Verbose output |
|
||||||
| `-h`, `--help` | Help |
|
| `-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`).
|
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`).
|
||||||
|
|
||||||
|
|||||||
@@ -19,12 +19,7 @@
|
|||||||
* xo-cli completions fish --install
|
* xo-cli completions fish --install
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { existsSync, readFileSync, appendFileSync, mkdirSync } from "node:fs";
|
||||||
existsSync,
|
|
||||||
readFileSync,
|
|
||||||
appendFileSync,
|
|
||||||
mkdirSync,
|
|
||||||
} from "node:fs";
|
|
||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
@@ -216,13 +211,15 @@ const shellConfigs: Record<
|
|||||||
> = {
|
> = {
|
||||||
bash: {
|
bash: {
|
||||||
configFile: join(homedir(), ".bashrc"),
|
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,
|
configDirPattern: /^\s*(?:export\s+)?XO_CONFIG_DIR=/m,
|
||||||
evalCommand: (binName) => `eval "$(${binName} completions bash)"`,
|
evalCommand: (binName) => `eval "$(${binName} completions bash)"`,
|
||||||
},
|
},
|
||||||
zsh: {
|
zsh: {
|
||||||
configFile: join(homedir(), ".zshrc"),
|
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,
|
configDirPattern: /^\s*(?:export\s+)?XO_CONFIG_DIR=/m,
|
||||||
evalCommand: (binName) => `eval "$(${binName} completions zsh)"`,
|
evalCommand: (binName) => `eval "$(${binName} completions zsh)"`,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ const DUST_THRESHOLD = 546n;
|
|||||||
/**
|
/**
|
||||||
* Serializes an invitation to pretty-printed JSON for file export.
|
* 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);
|
JSON.stringify(JSON.parse(serializeInvitation(invitation)), null, indent);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -358,8 +361,7 @@ export const handleInvitationExportCommand = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const invitation = deps.app.invitations.find(
|
const invitation = deps.app.invitations.find(
|
||||||
(candidate) =>
|
(candidate) => candidate.data.invitationIdentifier === invitationIdentifier,
|
||||||
candidate.data.invitationIdentifier === invitationIdentifier,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!invitation) {
|
if (!invitation) {
|
||||||
@@ -499,7 +501,9 @@ export const handleInvitationCommand = async (
|
|||||||
hasMissingRequirements(missingRequirements.templateRequirements) ||
|
hasMissingRequirements(missingRequirements.templateRequirements) ||
|
||||||
missingRequirements.inputsMissingSignatures.length > 0;
|
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}`);
|
deps.io.verbose(`Has missing requirements: ${hasMissing}`);
|
||||||
|
|
||||||
// If there are missing requirements, print them out
|
// If there are missing requirements, print them out
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ function formatResource(
|
|||||||
showReserved = false,
|
showReserved = false,
|
||||||
): string {
|
): string {
|
||||||
// Format the template
|
// Format the template
|
||||||
const template = resource.template ? dim(`[${generateTemplateIdentifier(resource.template)}]`) : "";
|
const template = resource.template
|
||||||
|
? dim(`[${generateTemplateIdentifier(resource.template)}]`)
|
||||||
|
: "";
|
||||||
|
|
||||||
// Format the outpoint
|
// Format the outpoint
|
||||||
const outpoint = bold(
|
const outpoint = bold(
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export const handleSettingsCommand = async (
|
|||||||
const value =
|
const value =
|
||||||
key === "currency"
|
key === "currency"
|
||||||
? settings.getCurrency()
|
? settings.getCurrency()
|
||||||
: settings.getDefaultMnemonic() ?? "";
|
: (settings.getDefaultMnemonic() ?? "");
|
||||||
deps.io.out(value);
|
deps.io.out(value);
|
||||||
return { key, value };
|
return { key, value };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import { generateTemplateIdentifier } from "@xo-cash/engine";
|
|||||||
import type { XOTemplate } from "@xo-cash/types";
|
import type { XOTemplate } from "@xo-cash/types";
|
||||||
|
|
||||||
import { bold, dim, formatObject } from "../utils.js";
|
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 { resolveTemplateReferences } from "../../utils/templates.js";
|
||||||
import type { CommandDependencies, CommandIO } from "./types.js";
|
import type { CommandDependencies, CommandIO } from "./types.js";
|
||||||
import { CommandError } from "./types.js";
|
import { CommandError } from "./types.js";
|
||||||
|
|||||||
@@ -181,16 +181,20 @@ async function main(): Promise<void> {
|
|||||||
|
|
||||||
// Create an App instance
|
// Create an App instance
|
||||||
io.verbose("Creating app instance...");
|
io.verbose("Creating app instance...");
|
||||||
const app = await AppService.create(mnemonic, {
|
const app = await AppService.create(
|
||||||
syncServerUrl: options["syncServerUrl"] ?? "http://localhost:3000",
|
mnemonic,
|
||||||
engineConfig: {
|
{
|
||||||
databasePath: options["databasePath"] ?? paths.dataDir,
|
syncServerUrl: options["syncServerUrl"] ?? "http://localhost:3000",
|
||||||
databaseFilename: options["databaseFilename"] ?? "xo-wallet.db",
|
engineConfig: {
|
||||||
|
databasePath: options["databasePath"] ?? paths.dataDir,
|
||||||
|
databaseFilename: options["databaseFilename"] ?? "xo-wallet.db",
|
||||||
|
},
|
||||||
|
invitationStoragePath:
|
||||||
|
options["invitationStoragePath"] ??
|
||||||
|
join(paths.dataDir, "xo-invitations.db"),
|
||||||
},
|
},
|
||||||
invitationStoragePath:
|
settings,
|
||||||
options["invitationStoragePath"] ??
|
);
|
||||||
join(paths.dataDir, "xo-invitations.db"),
|
|
||||||
}, settings);
|
|
||||||
io.verbose("App instance created");
|
io.verbose("App instance created");
|
||||||
|
|
||||||
// Start the app
|
// Start the app
|
||||||
|
|||||||
@@ -100,8 +100,12 @@ export class AppService extends EventEmitter<AppEventMap> {
|
|||||||
const templates = await engine.listImportedTemplates();
|
const templates = await engine.listImportedTemplates();
|
||||||
|
|
||||||
templates.forEach(async (template) => {
|
templates.forEach(async (template) => {
|
||||||
engine.updateUnspentOutputsForTemplate(generateTemplateIdentifier(template));
|
engine.updateUnspentOutputsForTemplate(
|
||||||
engine.subscribeToScriptHashForTemplate(generateTemplateIdentifier(template));
|
generateTemplateIdentifier(template),
|
||||||
|
);
|
||||||
|
engine.subscribeToScriptHashForTemplate(
|
||||||
|
generateTemplateIdentifier(template),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -127,7 +131,14 @@ export class AppService extends EventEmitter<AppEventMap> {
|
|||||||
});
|
});
|
||||||
const rates = await RatesService.create(settings);
|
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(
|
constructor(
|
||||||
@@ -298,9 +309,9 @@ export class AppService extends EventEmitter<AppEventMap> {
|
|||||||
|
|
||||||
async start(): Promise<void> {
|
async start(): Promise<void> {
|
||||||
// Start rates in the background so BCH -> fiat conversions become reactive in the TUI.
|
// Start rates in the background so BCH -> fiat conversions become reactive in the TUI.
|
||||||
this.rates.start().catch((err) =>
|
this.rates
|
||||||
console.error('Error starting rates service:', err),
|
.start()
|
||||||
);
|
.catch((err) => console.error("Error starting rates service:", err));
|
||||||
|
|
||||||
// Get the invitations db
|
// Get the invitations db
|
||||||
const invitationsDb = this.storage.child("invitations");
|
const invitationsDb = this.storage.child("invitations");
|
||||||
|
|||||||
@@ -95,7 +95,6 @@ export class HistoryService {
|
|||||||
private invitations: Invitation[],
|
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
|
* 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
|
* 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) {
|
for (const context of utxoContexts) {
|
||||||
const invitationIdentifier = context.utxo.reservedBy;
|
const invitationIdentifier = context.utxo.reservedBy;
|
||||||
if (invitationIdentifier && invitationContexts.has(invitationIdentifier)) {
|
if (
|
||||||
|
invitationIdentifier &&
|
||||||
|
invitationContexts.has(invitationIdentifier)
|
||||||
|
) {
|
||||||
const group = reservedUtxosByInvitation.get(invitationIdentifier) ?? [];
|
const group = reservedUtxosByInvitation.get(invitationIdentifier) ?? [];
|
||||||
group.push(context);
|
group.push(context);
|
||||||
reservedUtxosByInvitation.set(invitationIdentifier, group);
|
reservedUtxosByInvitation.set(invitationIdentifier, group);
|
||||||
@@ -141,13 +143,15 @@ export class HistoryService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async buildInvitationContextIndex(): Promise<Map<string, InvitationContext>> {
|
private async buildInvitationContextIndex(): Promise<
|
||||||
|
Map<string, InvitationContext>
|
||||||
|
> {
|
||||||
const contexts = new Map<string, InvitationContext>();
|
const contexts = new Map<string, InvitationContext>();
|
||||||
|
|
||||||
for (const invitation of this.invitations) {
|
for (const invitation of this.invitations) {
|
||||||
const templateIdentifier = invitation.data.templateIdentifier;
|
const templateIdentifier = invitation.data.templateIdentifier;
|
||||||
const template = templateIdentifier
|
const template = templateIdentifier
|
||||||
? (await this.engine.getTemplate(templateIdentifier)) ?? null
|
? ((await this.engine.getTemplate(templateIdentifier)) ?? null)
|
||||||
: null;
|
: null;
|
||||||
contexts.set(invitation.data.invitationIdentifier, {
|
contexts.set(invitation.data.invitationIdentifier, {
|
||||||
invitation,
|
invitation,
|
||||||
@@ -181,9 +185,13 @@ export class HistoryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const templateIdentifier of templateIdentifiers) {
|
for (const templateIdentifier of templateIdentifiers) {
|
||||||
const scriptHashDataList = await this.engine.listScriptHashesForTemplate(templateIdentifier);
|
const scriptHashDataList =
|
||||||
|
await this.engine.listScriptHashesForTemplate(templateIdentifier);
|
||||||
for (const scriptHashData of scriptHashDataList) {
|
for (const scriptHashData of scriptHashDataList) {
|
||||||
scriptHashDataByScriptHash.set(scriptHashData.scriptHash, scriptHashData);
|
scriptHashDataByScriptHash.set(
|
||||||
|
scriptHashData.scriptHash,
|
||||||
|
scriptHashData,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,10 +202,12 @@ export class HistoryService {
|
|||||||
utxo: UnspentOutputData,
|
utxo: UnspentOutputData,
|
||||||
metadataIndex: WalletMetadataIndex,
|
metadataIndex: WalletMetadataIndex,
|
||||||
): Promise<UtxoContext> {
|
): Promise<UtxoContext> {
|
||||||
const scriptHashData = metadataIndex.scriptHashDataByScriptHash.get(utxo.scriptHash);
|
const scriptHashData = metadataIndex.scriptHashDataByScriptHash.get(
|
||||||
|
utxo.scriptHash,
|
||||||
|
);
|
||||||
const templateIdentifier = scriptHashData?.templateIdentifier;
|
const templateIdentifier = scriptHashData?.templateIdentifier;
|
||||||
const template = templateIdentifier
|
const template = templateIdentifier
|
||||||
? (await this.engine.getTemplate(templateIdentifier)) ?? null
|
? ((await this.engine.getTemplate(templateIdentifier)) ?? null)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -213,8 +223,15 @@ export class HistoryService {
|
|||||||
): WalletHistoryItem {
|
): WalletHistoryItem {
|
||||||
const invitation = context.invitation.data;
|
const invitation = context.invitation.data;
|
||||||
const entityRoles = this.deriveInvitationEntityRoles(context);
|
const entityRoles = this.deriveInvitationEntityRoles(context);
|
||||||
const inputs = this.projectInvitationInputs(context, reservedContexts, entityRoles);
|
const inputs = this.projectInvitationInputs(
|
||||||
const inputUtxoIds = this.listInvitationInputUtxoIds(context, reservedContexts);
|
context,
|
||||||
|
reservedContexts,
|
||||||
|
entityRoles,
|
||||||
|
);
|
||||||
|
const inputUtxoIds = this.listInvitationInputUtxoIds(
|
||||||
|
context,
|
||||||
|
reservedContexts,
|
||||||
|
);
|
||||||
const outputs = this.projectInvitationOutputs(
|
const outputs = this.projectInvitationOutputs(
|
||||||
context,
|
context,
|
||||||
reservedContexts,
|
reservedContexts,
|
||||||
@@ -263,7 +280,9 @@ export class HistoryService {
|
|||||||
const outpointIndex = input.outpointIndex;
|
const outpointIndex = input.outpointIndex;
|
||||||
if (txid === undefined || outpointIndex === undefined) continue;
|
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.
|
// TODO: Remove this reservation-based filter once Engine/library cleanup releases stale invitation reservations internally.
|
||||||
if (!utxoContext) continue;
|
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.
|
// UTXO-first: committed outputs only matter here if they resolve to a wallet UTXO currently reserved by this invitation.
|
||||||
if (!matchingContext) continue;
|
if (!matchingContext) continue;
|
||||||
|
|
||||||
const lockingBytecode = this.getOutputLockingBytecodeHex(output) ?? matchingContext.scriptHashData?.lockingBytecode;
|
const lockingBytecode =
|
||||||
const outputIdentifier = output.outputIdentifier ?? matchingContext.scriptHashData?.outputIdentifier;
|
this.getOutputLockingBytecodeHex(output) ??
|
||||||
|
matchingContext.scriptHashData?.lockingBytecode;
|
||||||
|
const outputIdentifier =
|
||||||
|
output.outputIdentifier ??
|
||||||
|
matchingContext.scriptHashData?.outputIdentifier;
|
||||||
const role =
|
const role =
|
||||||
output.roleIdentifier ??
|
output.roleIdentifier ??
|
||||||
this.getFirstEntityRole(entityRoles, commit.entityIdentifier) ??
|
this.getFirstEntityRole(entityRoles, commit.entityIdentifier) ??
|
||||||
matchingContext.scriptHashData?.roleIdentifier;
|
matchingContext.scriptHashData?.roleIdentifier;
|
||||||
const valueSatoshis = output.valueSatoshis !== undefined
|
const valueSatoshis =
|
||||||
? BigInt(output.valueSatoshis)
|
output.valueSatoshis !== undefined
|
||||||
: BigInt(matchingContext.utxo.valueSatoshis);
|
? BigInt(output.valueSatoshis)
|
||||||
|
: BigInt(matchingContext.utxo.valueSatoshis);
|
||||||
|
|
||||||
usedUtxoIds.add(this.getUtxoId(matchingContext.utxo));
|
usedUtxoIds.add(this.getUtxoId(matchingContext.utxo));
|
||||||
|
|
||||||
@@ -369,8 +393,11 @@ export class HistoryService {
|
|||||||
const outpointIndex = input.outpointIndex;
|
const outpointIndex = input.outpointIndex;
|
||||||
if (txid === undefined || outpointIndex === undefined) continue;
|
if (txid === undefined || outpointIndex === undefined) continue;
|
||||||
|
|
||||||
const utxoContext = reservedByOutpoint.get(this.getOutpointKey(txid, outpointIndex));
|
const utxoContext = reservedByOutpoint.get(
|
||||||
if (utxoContext) invitationInputUtxoIds.add(this.getUtxoId(utxoContext.utxo));
|
this.getOutpointKey(txid, outpointIndex),
|
||||||
|
);
|
||||||
|
if (utxoContext)
|
||||||
|
invitationInputUtxoIds.add(this.getUtxoId(utxoContext.utxo));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,9 +417,17 @@ export class HistoryService {
|
|||||||
return reservedContexts.find((context) => {
|
return reservedContexts.find((context) => {
|
||||||
if (usedUtxoIds.has(this.getUtxoId(context.utxo))) return false;
|
if (usedUtxoIds.has(this.getUtxoId(context.utxo))) return false;
|
||||||
if (scriptHash && context.utxo.scriptHash === scriptHash) return true;
|
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;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -423,7 +458,11 @@ export class HistoryService {
|
|||||||
id: this.getUtxoId(context.utxo),
|
id: this.getUtxoId(context.utxo),
|
||||||
outputIdentifier,
|
outputIdentifier,
|
||||||
role,
|
role,
|
||||||
description: this.describeOutputFromTemplate(outputIdentifier, context.template, {}),
|
description: this.describeOutputFromTemplate(
|
||||||
|
outputIdentifier,
|
||||||
|
context.template,
|
||||||
|
{},
|
||||||
|
),
|
||||||
valueSatoshis: BigInt(context.utxo.valueSatoshis),
|
valueSatoshis: BigInt(context.utxo.valueSatoshis),
|
||||||
outpoint: {
|
outpoint: {
|
||||||
txid: context.utxo.outpointTransactionHash,
|
txid: context.utxo.outpointTransactionHash,
|
||||||
@@ -435,17 +474,22 @@ export class HistoryService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private deriveInvitationEntityRoles(context: InvitationContext): Map<string, string[]> {
|
private deriveInvitationEntityRoles(
|
||||||
|
context: InvitationContext,
|
||||||
|
): Map<string, string[]> {
|
||||||
const invitation = context.invitation.data;
|
const invitation = context.invitation.data;
|
||||||
const rolesByEntity = new Map<string, Set<string>>();
|
const rolesByEntity = new Map<string, Set<string>>();
|
||||||
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) {
|
for (const entityIdentifier of allEntities) {
|
||||||
rolesByEntity.set(entityIdentifier, new Set());
|
rolesByEntity.set(entityIdentifier, new Set());
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const commit of invitation.commits) {
|
for (const commit of invitation.commits) {
|
||||||
const roles = rolesByEntity.get(commit.entityIdentifier) ?? new Set<string>();
|
const roles =
|
||||||
|
rolesByEntity.get(commit.entityIdentifier) ?? new Set<string>();
|
||||||
for (const input of commit.data.inputs ?? []) {
|
for (const input of commit.data.inputs ?? []) {
|
||||||
if (input.roleIdentifier) roles.add(input.roleIdentifier);
|
if (input.roleIdentifier) roles.add(input.roleIdentifier);
|
||||||
}
|
}
|
||||||
@@ -459,9 +503,10 @@ export class HistoryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const action = context.template?.actions?.[invitation.actionIdentifier];
|
const action = context.template?.actions?.[invitation.actionIdentifier];
|
||||||
const participantRoles = action?.requirements?.participants
|
const participantRoles =
|
||||||
?.map((participant) => participant.role)
|
action?.requirements?.participants
|
||||||
.filter((role): role is string => typeof role === "string") ?? [];
|
?.map((participant) => participant.role)
|
||||||
|
.filter((role): role is string => typeof role === "string") ?? [];
|
||||||
const explicitlyFilledRoles = new Set<string>();
|
const explicitlyFilledRoles = new Set<string>();
|
||||||
for (const roles of rolesByEntity.values()) {
|
for (const roles of rolesByEntity.values()) {
|
||||||
for (const role of roles) explicitlyFilledRoles.add(role);
|
for (const role of roles) explicitlyFilledRoles.add(role);
|
||||||
@@ -473,7 +518,10 @@ export class HistoryService {
|
|||||||
.filter(([, roles]) => roles.size === 0)
|
.filter(([, roles]) => roles.size === 0)
|
||||||
.map(([entityIdentifier]) => entityIdentifier);
|
.map(([entityIdentifier]) => entityIdentifier);
|
||||||
|
|
||||||
if (unfilledParticipantRoles.length === 1 && entitiesWithoutRoles.length >= 1) {
|
if (
|
||||||
|
unfilledParticipantRoles.length === 1 &&
|
||||||
|
entitiesWithoutRoles.length >= 1
|
||||||
|
) {
|
||||||
const inferredRole = unfilledParticipantRoles[0];
|
const inferredRole = unfilledParticipantRoles[0];
|
||||||
if (inferredRole !== undefined) {
|
if (inferredRole !== undefined) {
|
||||||
for (const entityIdentifier of entitiesWithoutRoles) {
|
for (const entityIdentifier of entitiesWithoutRoles) {
|
||||||
@@ -517,12 +565,21 @@ export class HistoryService {
|
|||||||
inputs: WalletHistoryInput[],
|
inputs: WalletHistoryInput[],
|
||||||
outputs: WalletHistoryOutput[],
|
outputs: WalletHistoryOutput[],
|
||||||
): bigint {
|
): bigint {
|
||||||
const inputTotal = inputs.reduce((total, input) => total + (input.valueSatoshis ?? 0n), 0n);
|
const inputTotal = inputs.reduce(
|
||||||
const outputTotal = outputs.reduce((total, output) => total + (output.valueSatoshis ?? 0n), 0n);
|
(total, input) => total + (input.valueSatoshis ?? 0n),
|
||||||
|
0n,
|
||||||
|
);
|
||||||
|
const outputTotal = outputs.reduce(
|
||||||
|
(total, output) => total + (output.valueSatoshis ?? 0n),
|
||||||
|
0n,
|
||||||
|
);
|
||||||
return inputTotal + outputTotal;
|
return inputTotal + outputTotal;
|
||||||
}
|
}
|
||||||
|
|
||||||
private describeInvitation(context: InvitationContext, role?: string): string {
|
private describeInvitation(
|
||||||
|
context: InvitationContext,
|
||||||
|
role?: string,
|
||||||
|
): string {
|
||||||
const invitation = context.invitation.data;
|
const invitation = context.invitation.data;
|
||||||
const template = context.template;
|
const template = context.template;
|
||||||
if (!template) return invitation.actionIdentifier;
|
if (!template) return invitation.actionIdentifier;
|
||||||
@@ -544,14 +601,27 @@ export class HistoryService {
|
|||||||
return this.compileDescription(descriptionTemplate, context.variables);
|
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";
|
if (!inputIdentifier) return "Input";
|
||||||
const input = context.template?.inputs?.[inputIdentifier];
|
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 {
|
private describeOutput(
|
||||||
return this.describeOutputFromTemplate(outputIdentifier, context.template, context.variables);
|
outputIdentifier: string | undefined,
|
||||||
|
context: InvitationContext,
|
||||||
|
): string {
|
||||||
|
return this.describeOutputFromTemplate(
|
||||||
|
outputIdentifier,
|
||||||
|
context.template,
|
||||||
|
context.variables,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private describeOutputFromTemplate(
|
private describeOutputFromTemplate(
|
||||||
@@ -561,7 +631,10 @@ export class HistoryService {
|
|||||||
): string {
|
): string {
|
||||||
if (!outputIdentifier) return "Output";
|
if (!outputIdentifier) return "Output";
|
||||||
const output = template?.outputs?.[outputIdentifier];
|
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(
|
private compileDescription(
|
||||||
@@ -569,16 +642,25 @@ export class HistoryService {
|
|||||||
variables: Record<string, XOInvitationVariableValue>,
|
variables: Record<string, XOInvitationVariableValue>,
|
||||||
): string {
|
): string {
|
||||||
try {
|
try {
|
||||||
return compileCashAssemblyString({ cashAssemblyText: description, variables, evaluationDecodeMode: 'utf8' });
|
return compileCashAssemblyString({
|
||||||
|
cashAssemblyText: description,
|
||||||
|
variables,
|
||||||
|
evaluationDecodeMode: "utf8",
|
||||||
|
});
|
||||||
} catch {
|
} catch {
|
||||||
return this.interpolateSimpleCashAssemblyVariables(description, variables);
|
return this.interpolateSimpleCashAssemblyVariables(
|
||||||
|
description,
|
||||||
|
variables,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extractInvitationVariables(
|
private extractInvitationVariables(
|
||||||
invitation: XOInvitation,
|
invitation: XOInvitation,
|
||||||
): Record<string, XOInvitationVariableValue> {
|
): Record<string, XOInvitationVariableValue> {
|
||||||
const committedVariables = invitation.commits.flatMap((c) => c.data.variables ?? []);
|
const committedVariables = invitation.commits.flatMap(
|
||||||
|
(c) => c.data.variables ?? [],
|
||||||
|
);
|
||||||
return committedVariables.reduce(
|
return committedVariables.reduce(
|
||||||
(acc, variable) => {
|
(acc, variable) => {
|
||||||
if (!variable.variableIdentifier) return acc;
|
if (!variable.variableIdentifier) return acc;
|
||||||
@@ -596,15 +678,21 @@ export class HistoryService {
|
|||||||
: String(input.outpointTransactionHash);
|
: String(input.outpointTransactionHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getOutputLockingBytecodeHex(output: XOInvitationOutput): string | undefined {
|
private getOutputLockingBytecodeHex(
|
||||||
|
output: XOInvitationOutput,
|
||||||
|
): string | undefined {
|
||||||
if (output.lockingBytecode === undefined) return undefined;
|
if (output.lockingBytecode === undefined) return undefined;
|
||||||
return typeof output.lockingBytecode === "string"
|
return typeof output.lockingBytecode === "string"
|
||||||
? output.lockingBytecode
|
? output.lockingBytecode
|
||||||
: binToHex(output.lockingBytecode);
|
: binToHex(output.lockingBytecode);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getScriptHashData(scriptHash: string): Promise<ScriptHashData | undefined> {
|
private async getScriptHashData(
|
||||||
return (this.engine as unknown as { state: State }).state.getScriptHashData(scriptHash);
|
scriptHash: string,
|
||||||
|
): Promise<ScriptHashData | undefined> {
|
||||||
|
return (this.engine as unknown as { state: State }).state.getScriptHashData(
|
||||||
|
scriptHash,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getOutpointKey(txid: string, index: number): string {
|
private getOutpointKey(txid: string, index: number): string {
|
||||||
@@ -627,7 +715,9 @@ export class HistoryService {
|
|||||||
return text.replace(
|
return text.replace(
|
||||||
/\$\(<([^>]+)>\)/g,
|
/\$\(<([^>]+)>\)/g,
|
||||||
(match, variableIdentifier: string) => {
|
(match, variableIdentifier: string) => {
|
||||||
if (!Object.prototype.hasOwnProperty.call(variables, variableIdentifier)) {
|
if (
|
||||||
|
!Object.prototype.hasOwnProperty.call(variables, variableIdentifier)
|
||||||
|
) {
|
||||||
return match;
|
return match;
|
||||||
}
|
}
|
||||||
return String(variables[variableIdentifier]);
|
return String(variables[variableIdentifier]);
|
||||||
|
|||||||
@@ -3,7 +3,13 @@ import type {
|
|||||||
Engine,
|
Engine,
|
||||||
GetSpendableResourcesParameters,
|
GetSpendableResourcesParameters,
|
||||||
} from "@xo-cash/engine";
|
} 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 {
|
import type {
|
||||||
XOInvitation,
|
XOInvitation,
|
||||||
XOInvitationCommit,
|
XOInvitationCommit,
|
||||||
@@ -92,7 +98,9 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// engine invitation (I have no idea if this is required)
|
// 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
|
// Create the invitation
|
||||||
const invitationInstance = new Invitation(engineInvitation, dependencies);
|
const invitationInstance = new Invitation(engineInvitation, dependencies);
|
||||||
@@ -287,7 +295,9 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
|||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
private unwrapLegacyInvitationUpdatedPayload(payload: unknown): unknown | null {
|
private unwrapLegacyInvitationUpdatedPayload(
|
||||||
|
payload: unknown,
|
||||||
|
): unknown | null {
|
||||||
if (
|
if (
|
||||||
payload &&
|
payload &&
|
||||||
typeof payload === "object" &&
|
typeof payload === "object" &&
|
||||||
@@ -308,7 +318,10 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
|||||||
invitation: XOInvitation = this.data,
|
invitation: XOInvitation = this.data,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this.syncServer.publishInvitation(invitation).catch((error) => {
|
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<InvitationEventMap> {
|
|||||||
private async computeStatusInternal(): Promise<string> {
|
private async computeStatusInternal(): Promise<string> {
|
||||||
let missingReqs;
|
let missingReqs;
|
||||||
try {
|
try {
|
||||||
const missingRequirements = await this.engine.listMissingRequirements(this.data.invitationIdentifier);
|
const missingRequirements = await this.engine.listMissingRequirements(
|
||||||
|
this.data.invitationIdentifier,
|
||||||
|
);
|
||||||
missingReqs = missingRequirements.templateRequirements;
|
missingReqs = missingRequirements.templateRequirements;
|
||||||
} catch {
|
} catch {
|
||||||
return "unknown";
|
return "unknown";
|
||||||
@@ -454,13 +469,18 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
|||||||
* Update the status of the invitation and emit the new single-word status.
|
* Update the status of the invitation and emit the new single-word status.
|
||||||
*/
|
*/
|
||||||
private async updateStatus(): Promise<void> {
|
private async updateStatus(): Promise<void> {
|
||||||
this.computeStatus().then(status => {
|
this.computeStatus()
|
||||||
this.status = status;
|
.then((status) => {
|
||||||
this.emit("invitation-status-changed", status);
|
this.status = status;
|
||||||
}).catch((error) => {
|
this.emit("invitation-status-changed", status);
|
||||||
this.status = `error (${error instanceof Error ? error.message : String(error)})`;
|
})
|
||||||
this.emit("error", error instanceof Error ? error : new Error(String(error)));
|
.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<InvitationEventMap> {
|
|||||||
*/
|
*/
|
||||||
async sign(): Promise<void> {
|
async sign(): Promise<void> {
|
||||||
// Sign the invitation
|
// 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
|
// Publish the signed invitation to the sync server
|
||||||
this.publishInvitation(signedInvitation);
|
this.publishInvitation(signedInvitation);
|
||||||
@@ -518,9 +540,12 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
|||||||
* @returns The transaction hash returned by the network after broadcast.
|
* @returns The transaction hash returned by the network after broadcast.
|
||||||
*/
|
*/
|
||||||
async broadcast(): Promise<string> {
|
async broadcast(): Promise<string> {
|
||||||
const txHash = await this.engine.executeAction(this.data.invitationIdentifier, {
|
const txHash = await this.engine.executeAction(
|
||||||
broadcastTransaction: true,
|
this.data.invitationIdentifier,
|
||||||
});
|
{
|
||||||
|
broadcastTransaction: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
await this.updateStatus();
|
await this.updateStatus();
|
||||||
|
|
||||||
@@ -538,7 +563,10 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
|||||||
await this.ensureAccepted();
|
await this.ensureAccepted();
|
||||||
|
|
||||||
// Append the commit to the invitation
|
// 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
|
// Sync the invitation to the sync server
|
||||||
await this.publishInvitation(this.data);
|
await this.publishInvitation(this.data);
|
||||||
@@ -617,8 +645,8 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
|||||||
const templates = await this.engine.listImportedTemplates();
|
const templates = await this.engine.listImportedTemplates();
|
||||||
|
|
||||||
// For each template, we need to create a 2d array of all the outputs
|
// For each template, we need to create a 2d array of all the outputs
|
||||||
const outputs = templates.map(template => {
|
const outputs = templates.map((template) => {
|
||||||
return Object.keys(template.outputs).map(output => {
|
return Object.keys(template.outputs).map((output) => {
|
||||||
const templateIdentifier = generateTemplateIdentifier(template);
|
const templateIdentifier = generateTemplateIdentifier(template);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -629,14 +657,18 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// then, for each output, we need to get the spendable resources
|
// then, for each output, we need to get the spendable resources
|
||||||
const spendableResources = await Promise.all(outputs.flat().map(output => {
|
const spendableResources = await Promise.all(
|
||||||
return this.engine.getSpendableResources(this.data, {
|
outputs.flat().map((output) => {
|
||||||
templateIdentifier: output.templateIdentifier,
|
return this.engine.getSpendableResources(this.data, {
|
||||||
outputIdentifier: output.outputIdentifier,
|
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
|
// Update the status of the invitation
|
||||||
await this.updateStatus();
|
await this.updateStatus();
|
||||||
@@ -738,9 +770,11 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Compile the CashAssembly expression to get the value satoshis (It handles the variable replacement for us)
|
// Compile the CashAssembly expression to get the value satoshis (It handles the variable replacement for us)
|
||||||
const valueSatoshis = compileCashAssemblyString(
|
const valueSatoshis = compileCashAssemblyString({
|
||||||
{ cashAssemblyText: String(valueSatoshisExpression), variables: formattedVariables, evaluationDecodeMode: 'bigint' },
|
cashAssemblyText: String(valueSatoshisExpression),
|
||||||
);
|
variables: formattedVariables,
|
||||||
|
evaluationDecodeMode: "bigint",
|
||||||
|
});
|
||||||
|
|
||||||
// Return the value satoshis as a 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
|
// 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<InvitationEventMap> {
|
|||||||
for (const output of outputs) {
|
for (const output of outputs) {
|
||||||
if (typeof output === "string") {
|
if (typeof output === "string") {
|
||||||
const sats = await this.getSatsOut(output);
|
const sats = await this.getSatsOut(output);
|
||||||
totalSats += sats
|
totalSats += sats;
|
||||||
} else {
|
} else {
|
||||||
const sats = await this.getSatsOut(output.output);
|
const sats = await this.getSatsOut(output.output);
|
||||||
totalSats += sats;
|
totalSats += sats;
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import { OracleClient } from '@generalprotocols/oracle-client';
|
import { OracleClient } from "@generalprotocols/oracle-client";
|
||||||
import { EventEmitter } from '../utils/event-emitter.js';
|
import { EventEmitter } from "../utils/event-emitter.js";
|
||||||
import {
|
import { type RatesEventMap } from "../utils/rates/base-rates.js";
|
||||||
type RatesEventMap,
|
import { RatesOracle } from "../utils/rates/rates-oracles.js";
|
||||||
} from '../utils/rates/base-rates.js';
|
import { SettingsService } from "./settings.js";
|
||||||
import { RatesOracle } from '../utils/rates/rates-oracles.js';
|
|
||||||
import { SettingsService } from './settings.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event map emitted by {@link RatesService}.
|
* Event map emitted by {@link RatesService}.
|
||||||
*/
|
*/
|
||||||
export type RatesServiceEventMap = {
|
export type RatesServiceEventMap = {
|
||||||
'rate-updated': {
|
"rate-updated": {
|
||||||
numeratorUnitCode: string;
|
numeratorUnitCode: string;
|
||||||
denominatorUnitCode: string;
|
denominatorUnitCode: string;
|
||||||
price: number;
|
price: number;
|
||||||
@@ -39,8 +37,8 @@ export interface RatesAdapter {
|
|||||||
listPairs(): Promise<Set<string>>;
|
listPairs(): Promise<Set<string>>;
|
||||||
formatCurrency(amount: number, targetCurrency: string): string;
|
formatCurrency(amount: number, targetCurrency: string): string;
|
||||||
on(
|
on(
|
||||||
type: 'rateUpdated',
|
type: "rateUpdated",
|
||||||
listener: (detail: RatesEventMap['rateUpdated']) => void,
|
listener: (detail: RatesEventMap["rateUpdated"]) => void,
|
||||||
): () => void;
|
): () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +94,7 @@ export class RatesService extends EventEmitter<RatesServiceEventMap> {
|
|||||||
}
|
}
|
||||||
this.started = true;
|
this.started = true;
|
||||||
|
|
||||||
this.unsubscribeFromAdapter = this.adapter.on('rateUpdated', (event) => {
|
this.unsubscribeFromAdapter = this.adapter.on("rateUpdated", (event) => {
|
||||||
this.handleRateUpdated(event);
|
this.handleRateUpdated(event);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -145,9 +143,9 @@ export class RatesService extends EventEmitter<RatesServiceEventMap> {
|
|||||||
*/
|
*/
|
||||||
public convertBchToFiat(
|
public convertBchToFiat(
|
||||||
satoshis: bigint,
|
satoshis: bigint,
|
||||||
targetCurrency: string = 'USD',
|
targetCurrency: string = "USD",
|
||||||
): number | null {
|
): number | null {
|
||||||
const rate = this.getRate(targetCurrency, 'BCH');
|
const rate = this.getRate(targetCurrency, "BCH");
|
||||||
if (rate === null) {
|
if (rate === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -161,7 +159,7 @@ export class RatesService extends EventEmitter<RatesServiceEventMap> {
|
|||||||
*/
|
*/
|
||||||
public formatBchToFiat(
|
public formatBchToFiat(
|
||||||
satoshis: bigint,
|
satoshis: bigint,
|
||||||
targetCurrency: string = 'USD',
|
targetCurrency: string = "USD",
|
||||||
): string | null {
|
): string | null {
|
||||||
const normalizedCurrency = targetCurrency.toUpperCase();
|
const normalizedCurrency = targetCurrency.toUpperCase();
|
||||||
const amount = this.convertBchToFiat(satoshis, normalizedCurrency);
|
const amount = this.convertBchToFiat(satoshis, normalizedCurrency);
|
||||||
@@ -195,7 +193,7 @@ export class RatesService extends EventEmitter<RatesServiceEventMap> {
|
|||||||
/**
|
/**
|
||||||
* Handles normalized updates from the underlying adapter.
|
* 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 numeratorUnitCode = event.numeratorUnitCode.toUpperCase();
|
||||||
const denominatorUnitCode = event.denominatorUnitCode.toUpperCase();
|
const denominatorUnitCode = event.denominatorUnitCode.toUpperCase();
|
||||||
const pair = this.getPairKey(numeratorUnitCode, denominatorUnitCode);
|
const pair = this.getPairKey(numeratorUnitCode, denominatorUnitCode);
|
||||||
@@ -206,7 +204,7 @@ export class RatesService extends EventEmitter<RatesServiceEventMap> {
|
|||||||
updatedAt,
|
updatedAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.emit('rate-updated', {
|
this.emit("rate-updated", {
|
||||||
numeratorUnitCode,
|
numeratorUnitCode,
|
||||||
denominatorUnitCode,
|
denominatorUnitCode,
|
||||||
price: event.price,
|
price: event.price,
|
||||||
|
|||||||
@@ -168,7 +168,9 @@ export class SettingsService extends EventEmitter<SettingsServiceEventMap> {
|
|||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
const maybeMnemonic = (input as Record<string, unknown>)["default-mnemonic"];
|
const maybeMnemonic = (input as Record<string, unknown>)[
|
||||||
|
"default-mnemonic"
|
||||||
|
];
|
||||||
if (typeof maybeMnemonic === "string" && maybeMnemonic.trim().length > 0) {
|
if (typeof maybeMnemonic === "string" && maybeMnemonic.trim().length > 0) {
|
||||||
normalized["default-mnemonic"] = maybeMnemonic.trim();
|
normalized["default-mnemonic"] = maybeMnemonic.trim();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { XOTemplate } from '@xo-cash/types';
|
import type { XOTemplate } from "@xo-cash/types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vending machine payment template.
|
* Vending machine payment template.
|
||||||
@@ -7,271 +7,277 @@ import type { XOTemplate } from '@xo-cash/types';
|
|||||||
* customer funds and signs the composable transaction.
|
* customer funds and signs the composable transaction.
|
||||||
*/
|
*/
|
||||||
export const vendingMachineTemplate: XOTemplate = {
|
export const vendingMachineTemplate: XOTemplate = {
|
||||||
$schema: 'https://libauth.org/schemas/wallet-template-v0.schema.json',
|
$schema: "https://libauth.org/schemas/wallet-template-v0.schema.json",
|
||||||
name: 'Vending Machine',
|
name: "Vending Machine",
|
||||||
description: 'Purchase items from a vending machine with an itemized receipt.',
|
description:
|
||||||
icon: 'wallet',
|
"Purchase items from a vending machine with an itemized receipt.",
|
||||||
version: '1',
|
icon: "wallet",
|
||||||
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"],
|
||||||
|
|
||||||
defaults: {
|
defaults: {
|
||||||
change: {
|
change: {
|
||||||
output: 'changeOutput',
|
output: "changeOutput",
|
||||||
role: 'merchant',
|
role: "merchant",
|
||||||
generate: ['merchantKey'],
|
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: $(<receiptSummary>) for $(<totalSatoshis>) sats",
|
||||||
|
icon: "request",
|
||||||
|
|
||||||
|
roles: {
|
||||||
merchant: {
|
merchant: {
|
||||||
name: 'Merchant',
|
name: "Sell Items",
|
||||||
description: 'The vending machine operator receiving payment.',
|
description: "Receive payment for $(<receiptSummary>)",
|
||||||
icon: 'owner',
|
icon: "request",
|
||||||
|
requirements: {
|
||||||
|
secrets: ["merchantKey"],
|
||||||
|
variables: [
|
||||||
|
"totalSatoshis",
|
||||||
|
"orderId",
|
||||||
|
"merchantName",
|
||||||
|
"receiptSummary",
|
||||||
|
"lineItemsJson",
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
customer: {
|
customer: {
|
||||||
name: 'Customer',
|
name: "Pay",
|
||||||
description: 'The customer paying for items.',
|
description: "Pay $(<totalSatoshis>) sats for $(<receiptSummary>)",
|
||||||
icon: 'sender',
|
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 $(<orderId>): $(<receiptSummary>)",
|
||||||
|
icon: "request",
|
||||||
|
|
||||||
|
roles: {
|
||||||
|
merchant: {
|
||||||
|
name: "Received Payment",
|
||||||
|
description:
|
||||||
|
"Received $(<totalSatoshis>) sats from $(<merchantName>) sale",
|
||||||
|
icon: "receive",
|
||||||
|
},
|
||||||
|
customer: {
|
||||||
|
name: "Sent Payment",
|
||||||
|
description: "Paid $(<totalSatoshis>) sats for $(<receiptSummary>)",
|
||||||
|
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: "$(<totalSatoshis>) sats to $(<merchantName>)",
|
||||||
|
icon: "request",
|
||||||
|
|
||||||
|
roles: {
|
||||||
|
merchant: {
|
||||||
|
name: "Payment Received",
|
||||||
|
description:
|
||||||
|
"Received $(<totalSatoshis>) sats for $(<receiptSummary>)",
|
||||||
|
},
|
||||||
|
customer: {
|
||||||
|
name: "Payment Sent",
|
||||||
|
description: "Sent $(<totalSatoshis>) sats for $(<receiptSummary>)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
lockingScript: "merchantReceivingLockingScript",
|
||||||
|
valueSatoshis: "$(<totalSatoshis>)",
|
||||||
|
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 <$(<merchantKey.public_key> OP_HASH160)> OP_EQUALVERIFY OP_CHECKSIG",
|
||||||
|
unlockMerchantP2PKH:
|
||||||
|
"<merchantKey.schnorr_signature.all_outputs> <merchantKey.public_key>",
|
||||||
|
},
|
||||||
|
|
||||||
|
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",
|
||||||
role: 'merchant',
|
values: {
|
||||||
generate: ['merchantKey'],
|
generated: {
|
||||||
},
|
merchantKey:
|
||||||
],
|
"KyRQa5pEXuzVcDwnXRLpYAascjchQW5DoxVRMbj4DTxS83573mz8",
|
||||||
|
|
||||||
actions: {
|
|
||||||
purchaseItems: {
|
|
||||||
name: 'Purchase Items',
|
|
||||||
description: 'Purchase: $(<receiptSummary>) for $(<totalSatoshis>) sats',
|
|
||||||
icon: 'request',
|
|
||||||
|
|
||||||
roles: {
|
|
||||||
merchant: {
|
|
||||||
name: 'Sell Items',
|
|
||||||
description: 'Receive payment for $(<receiptSummary>)',
|
|
||||||
icon: 'request',
|
|
||||||
requirements: {
|
|
||||||
secrets: ['merchantKey'],
|
|
||||||
variables: [
|
|
||||||
'totalSatoshis',
|
|
||||||
'orderId',
|
|
||||||
'merchantName',
|
|
||||||
'receiptSummary',
|
|
||||||
'lineItemsJson',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
customer: {
|
|
||||||
name: 'Pay',
|
|
||||||
description: 'Pay $(<totalSatoshis>) sats for $(<receiptSummary>)',
|
|
||||||
icon: 'send',
|
|
||||||
requirements: {},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
variables: {
|
||||||
requirements: {
|
totalSatoshis: 3500,
|
||||||
participants: [
|
orderId: "order-demo-1",
|
||||||
{ role: 'merchant', slots: { min: 1, max: 1 } },
|
merchantName: "XO Snack Machine",
|
||||||
{ role: 'customer', slots: { min: 1 } },
|
receiptSummary: "2× Cola, 1× Chips",
|
||||||
],
|
lineItemsJson:
|
||||||
|
'[{"name":"Cola","qty":2},{"name":"Chips","qty":1}]',
|
||||||
},
|
},
|
||||||
|
secrets: {},
|
||||||
transaction: 'purchaseItemsTransaction',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
transactions: {
|
|
||||||
purchaseItemsTransaction: {
|
|
||||||
name: 'Vending Purchase',
|
|
||||||
description: 'Order $(<orderId>): $(<receiptSummary>)',
|
|
||||||
icon: 'request',
|
|
||||||
|
|
||||||
roles: {
|
|
||||||
merchant: {
|
|
||||||
name: 'Received Payment',
|
|
||||||
description: 'Received $(<totalSatoshis>) sats from $(<merchantName>) sale',
|
|
||||||
icon: 'receive',
|
|
||||||
},
|
|
||||||
customer: {
|
|
||||||
name: 'Sent Payment',
|
|
||||||
description: 'Paid $(<totalSatoshis>) sats for $(<receiptSummary>)',
|
|
||||||
icon: 'send',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
inputs: [],
|
inputs: [],
|
||||||
outputs: [{ output: 'purchaseOutput' }],
|
outputs: [
|
||||||
version: 2,
|
{
|
||||||
locktime: 0,
|
lockingBytecode:
|
||||||
composable: true,
|
"76a91475c715ecb74178fe87933e57e947e5e92d904b8188ac",
|
||||||
},
|
valueSatoshis: 3500,
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 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: '$(<totalSatoshis>) sats to $(<merchantName>)',
|
|
||||||
icon: 'request',
|
|
||||||
|
|
||||||
roles: {
|
|
||||||
merchant: {
|
|
||||||
name: 'Payment Received',
|
|
||||||
description: 'Received $(<totalSatoshis>) sats for $(<receiptSummary>)',
|
|
||||||
},
|
|
||||||
customer: {
|
|
||||||
name: 'Payment Sent',
|
|
||||||
description: 'Sent $(<totalSatoshis>) sats for $(<receiptSummary>)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
lockingScript: 'merchantReceivingLockingScript',
|
|
||||||
valueSatoshis: '$(<totalSatoshis>)',
|
|
||||||
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 <$(<merchantKey.public_key> OP_HASH160)> OP_EQUALVERIFY OP_CHECKSIG',
|
|
||||||
unlockMerchantP2PKH:
|
|
||||||
'<merchantKey.schnorr_signature.all_outputs> <merchantKey.public_key>',
|
|
||||||
},
|
|
||||||
|
|
||||||
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: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
{
|
||||||
|
role: "customer",
|
||||||
|
values: {
|
||||||
|
generated: {},
|
||||||
|
variables: {},
|
||||||
|
secrets: {},
|
||||||
|
inputs: [],
|
||||||
|
outputs: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,266 +1,270 @@
|
|||||||
import type { XOTemplate } from '@xo-cash/types';
|
import type { XOTemplate } from "@xo-cash/types";
|
||||||
|
|
||||||
export const wrapBCHTemplate: XOTemplate = {
|
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',
|
name: "Wrapped BCH",
|
||||||
description: 'Convert between BCH and wBCH tokens.',
|
description: "Convert between BCH and wBCH tokens.",
|
||||||
icon: 'wrap',
|
icon: "wrap",
|
||||||
|
|
||||||
version: '1',
|
version: "1",
|
||||||
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"],
|
||||||
|
|
||||||
roles: {
|
roles: {
|
||||||
user: {
|
user: {
|
||||||
name: 'User',
|
name: "User",
|
||||||
description: 'The person wrapping or unwrapping BCH.',
|
description: "The person wrapping or unwrapping BCH.",
|
||||||
icon: 'user',
|
icon: "user",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
start: [
|
start: [
|
||||||
{
|
{
|
||||||
action: 'wrap',
|
action: "wrap",
|
||||||
role: 'user',
|
role: "user",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'unwrap',
|
action: "unwrap",
|
||||||
role: 'user',
|
role: "user",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
wrap: {
|
wrap: {
|
||||||
name: 'Wrap BCH',
|
name: "Wrap BCH",
|
||||||
description: 'Convert BCH into wBCH tokens.',
|
description: "Convert BCH into wBCH tokens.",
|
||||||
icon: 'wrap',
|
icon: "wrap",
|
||||||
|
|
||||||
roles: {
|
roles: {
|
||||||
user: {
|
user: {
|
||||||
requirements: {
|
requirements: {
|
||||||
variables: ['amountToWrap', 'recipientLockingScript'],
|
variables: ["amountToWrap", "recipientLockingScript"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
requirements: {
|
requirements: {
|
||||||
participants: [{ role: 'user', slots: { min: 1, max: 1 } }],
|
participants: [{ role: "user", slots: { min: 1, max: 1 } }],
|
||||||
},
|
},
|
||||||
|
|
||||||
transaction: 'wrapTransaction',
|
transaction: "wrapTransaction",
|
||||||
},
|
},
|
||||||
|
|
||||||
unwrap: {
|
unwrap: {
|
||||||
name: 'Unwrap wBCH',
|
name: "Unwrap wBCH",
|
||||||
description: 'Convert wBCH tokens back into BCH.',
|
description: "Convert wBCH tokens back into BCH.",
|
||||||
icon: 'unwrap',
|
icon: "unwrap",
|
||||||
|
|
||||||
roles: {
|
roles: {
|
||||||
user: {
|
user: {
|
||||||
requirements: {
|
requirements: {
|
||||||
variables: ['amountToUnwrap', 'recipientLockingScript'],
|
variables: ["amountToUnwrap", "recipientLockingScript"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
requirements: {
|
requirements: {
|
||||||
participants: [{ role: 'user', slots: { min: 1, max: 1 } }],
|
participants: [{ role: "user", slots: { min: 1, max: 1 } }],
|
||||||
},
|
},
|
||||||
|
|
||||||
transaction: 'unwrapTransaction',
|
transaction: "unwrapTransaction",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
transactions: {
|
transactions: {
|
||||||
wrapTransaction: {
|
wrapTransaction: {
|
||||||
name: 'Wrapped BCH',
|
name: "Wrapped BCH",
|
||||||
description: 'Wrapped $(<amountToWrap> <satoshisPerBCH> OP_DIV).$(<amountToWrap> <satoshisPerBCH> OP_MOD) BCH into wBCH tokens.',
|
description:
|
||||||
icon: 'wrap',
|
"Wrapped $(<amountToWrap> <satoshisPerBCH> OP_DIV).$(<amountToWrap> <satoshisPerBCH> OP_MOD) BCH into wBCH tokens.",
|
||||||
|
icon: "wrap",
|
||||||
|
|
||||||
inputs: [
|
inputs: [{ input: "covenantInput", inputIndex: 0 }],
|
||||||
{ input: 'covenantInput', inputIndex: 0 },
|
outputs: [
|
||||||
],
|
{ output: "covenantOutput", outputIndex: 0 },
|
||||||
outputs: [
|
{ output: "wrappedTokensOutput", outputIndex: undefined },
|
||||||
{ output: 'covenantOutput', outputIndex: 0 },
|
],
|
||||||
{ output: 'wrappedTokensOutput', outputIndex: undefined },
|
|
||||||
],
|
|
||||||
|
|
||||||
version: 2,
|
version: 2,
|
||||||
locktime: 0,
|
locktime: 0,
|
||||||
composable: true,
|
composable: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
unwrapTransaction: {
|
unwrapTransaction: {
|
||||||
name: 'Unwrapped wBCH',
|
name: "Unwrapped wBCH",
|
||||||
description: 'Unwrapped $(<amountToUnwrap> <satoshisPerBCH> OP_DIV).$(<amountToUnwrap> <satoshisPerBCH> OP_MOD) wBCH tokens back into BCH.',
|
description:
|
||||||
icon: 'unwrap',
|
"Unwrapped $(<amountToUnwrap> <satoshisPerBCH> OP_DIV).$(<amountToUnwrap> <satoshisPerBCH> OP_MOD) wBCH tokens back into BCH.",
|
||||||
|
icon: "unwrap",
|
||||||
|
|
||||||
inputs: [
|
inputs: [{ input: "covenantInput", inputIndex: 0 }],
|
||||||
{ input: 'covenantInput', inputIndex: 0 },
|
outputs: [
|
||||||
],
|
{ output: "covenantOutput", outputIndex: 0 },
|
||||||
outputs: [
|
{ output: "unwrappedSatoshisOutput", outputIndex: undefined },
|
||||||
{ output: 'covenantOutput', outputIndex: 0 },
|
],
|
||||||
{ output: 'unwrappedSatoshisOutput', outputIndex: undefined },
|
|
||||||
],
|
|
||||||
|
|
||||||
version: 2,
|
version: 2,
|
||||||
locktime: 0,
|
locktime: 0,
|
||||||
composable: true,
|
composable: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
outputs: {
|
outputs: {
|
||||||
covenantOutput: {
|
covenantOutput: {
|
||||||
name: 'wBCH Covenant',
|
name: "wBCH Covenant",
|
||||||
description: 'Holds BCH and wBCH tokens that can be freely converted.',
|
description: "Holds BCH and wBCH tokens that can be freely converted.",
|
||||||
icon: 'contract',
|
icon: "contract",
|
||||||
|
|
||||||
lockingScript: 'wrapBCHLockingScript',
|
lockingScript: "wrapBCHLockingScript",
|
||||||
},
|
},
|
||||||
|
|
||||||
wrappedTokensOutput: {
|
wrappedTokensOutput: {
|
||||||
name: 'Wrapped wBCH',
|
name: "Wrapped wBCH",
|
||||||
description: 'Wrapped $(<amountToWrap> <satoshisPerBCH> OP_DIV).$(<amountToWrap> <satoshisPerBCH> OP_MOD) wBCH tokens.',
|
description:
|
||||||
icon: 'receive',
|
"Wrapped $(<amountToWrap> <satoshisPerBCH> OP_DIV).$(<amountToWrap> <satoshisPerBCH> OP_MOD) wBCH tokens.",
|
||||||
|
icon: "receive",
|
||||||
|
|
||||||
valueSatoshis: '$(<amountToWrap>)',
|
valueSatoshis: "$(<amountToWrap>)",
|
||||||
token: {
|
token: {
|
||||||
category: '$(<wbchTokenCategory>)',
|
category: "$(<wbchTokenCategory>)",
|
||||||
amount: '$(<amountToWrap>)',
|
amount: "$(<amountToWrap>)",
|
||||||
nft: null,
|
nft: null,
|
||||||
},
|
},
|
||||||
|
|
||||||
roles: {
|
roles: {
|
||||||
user: {
|
user: {
|
||||||
balance: {
|
balance: {
|
||||||
satoshis: true,
|
satoshis: true,
|
||||||
fungibleTokens: true,
|
fungibleTokens: true,
|
||||||
nonfungibleTokens: true,
|
nonfungibleTokens: true,
|
||||||
},
|
},
|
||||||
selectable: true,
|
selectable: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
lockingScript: '$(<recipientLockingScript>)',
|
lockingScript: "$(<recipientLockingScript>)",
|
||||||
},
|
},
|
||||||
|
|
||||||
unwrappedSatoshisOutput: {
|
unwrappedSatoshisOutput: {
|
||||||
name: 'Unwrapped BCH',
|
name: "Unwrapped BCH",
|
||||||
description: 'Unwrapped $(<amountToUnwrap> <satoshisPerBCH> OP_DIV).$(<amountToUnwrap> <satoshisPerBCH> OP_MOD) BCH.',
|
description:
|
||||||
icon: 'receive',
|
"Unwrapped $(<amountToUnwrap> <satoshisPerBCH> OP_DIV).$(<amountToUnwrap> <satoshisPerBCH> OP_MOD) BCH.",
|
||||||
|
icon: "receive",
|
||||||
|
|
||||||
valueSatoshis: '$(<amountToUnwrap>)',
|
valueSatoshis: "$(<amountToUnwrap>)",
|
||||||
token: null,
|
token: null,
|
||||||
|
|
||||||
roles: {
|
roles: {
|
||||||
user: {
|
user: {
|
||||||
balance: {
|
balance: {
|
||||||
satoshis: true,
|
satoshis: true,
|
||||||
fungibleTokens: true,
|
fungibleTokens: true,
|
||||||
nonfungibleTokens: true,
|
nonfungibleTokens: true,
|
||||||
},
|
},
|
||||||
selectable: true,
|
selectable: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
lockingScript: '$(<recipientLockingScript>)',
|
lockingScript: "$(<recipientLockingScript>)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
inputs: {
|
inputs: {
|
||||||
covenantInput: {
|
covenantInput: {
|
||||||
name: 'wBCH Covenant',
|
name: "wBCH Covenant",
|
||||||
description: 'The covenant being updated.',
|
description: "The covenant being updated.",
|
||||||
icon: 'contract',
|
icon: "contract",
|
||||||
|
|
||||||
unlockingScript: 'unlockCovenant',
|
unlockingScript: "unlockCovenant",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
lockingScripts: {
|
lockingScripts: {
|
||||||
wrapBCHLockingScript: {
|
wrapBCHLockingScript: {
|
||||||
name: 'wBCH Covenant',
|
name: "wBCH Covenant",
|
||||||
description: 'Holds BCH and wBCH tokens that can be freely converted.',
|
description: "Holds BCH and wBCH tokens that can be freely converted.",
|
||||||
icon: 'contract',
|
icon: "contract",
|
||||||
|
|
||||||
lockingType: 'p2sh',
|
lockingType: "p2sh",
|
||||||
lockingBytecode: 'wrapBCHLockingBytecode',
|
lockingBytecode: "wrapBCHLockingBytecode",
|
||||||
|
|
||||||
actions: [
|
actions: [
|
||||||
{ action: 'wrap', role: 'user' },
|
{ action: "wrap", role: "user" },
|
||||||
{ action: 'unwrap', role: 'user' },
|
{ action: "unwrap", role: "user" },
|
||||||
],
|
],
|
||||||
|
|
||||||
state: {
|
state: {
|
||||||
variables: [],
|
variables: [],
|
||||||
secrets: [],
|
secrets: [],
|
||||||
},
|
},
|
||||||
balance: {
|
balance: {
|
||||||
satoshis: 0n,
|
satoshis: 0n,
|
||||||
fungibleTokens: 0n,
|
fungibleTokens: 0n,
|
||||||
},
|
},
|
||||||
selectable: false,
|
selectable: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
scripts: {
|
scripts: {
|
||||||
enforceCovenantPersists: 'OP_INPUTINDEX OP_DUP OP_OUTPUTBYTECODE OP_SWAP OP_UTXOBYTECODE OP_EQUAL OP_VERIFY',
|
enforceCovenantPersists:
|
||||||
enforceTokenCategoryPreserved: 'OP_INPUTINDEX OP_DUP OP_OUTPUTTOKENCATEGORY OP_SWAP OP_UTXOTOKENCATEGORY OP_EQUAL OP_VERIFY',
|
"OP_INPUTINDEX OP_DUP OP_OUTPUTBYTECODE OP_SWAP OP_UTXOBYTECODE 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',
|
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
|
// Direct script references — introspection opcodes must not use $(...) evaluations
|
||||||
// because those are evaluated at compile time without transaction context.
|
// because those are evaluated at compile time without transaction context.
|
||||||
wrapBCHLockingBytecode: 'enforceCovenantPersists enforceTokenCategoryPreserved enforceValueTokenSumConserved',
|
wrapBCHLockingBytecode:
|
||||||
unlockCovenant: '',
|
"enforceCovenantPersists enforceTokenCategoryPreserved enforceValueTokenSumConserved",
|
||||||
},
|
unlockCovenant: "",
|
||||||
|
},
|
||||||
|
|
||||||
constants: {
|
constants: {
|
||||||
wbchTokenCategory: {
|
wbchTokenCategory: {
|
||||||
name: 'wBCH Token Category',
|
name: "wBCH Token Category",
|
||||||
description: 'The official token category for Wrapped BCH.',
|
description: "The official token category for Wrapped BCH.",
|
||||||
type: 'bytes',
|
type: "bytes",
|
||||||
value: 'ff4d6e4b90aa8158d39c5dc874fd9411af1ac3b5ed6f354755e8362a0d02c6b3',
|
value: "ff4d6e4b90aa8158d39c5dc874fd9411af1ac3b5ed6f354755e8362a0d02c6b3",
|
||||||
},
|
},
|
||||||
satoshisPerBCH: {
|
satoshisPerBCH: {
|
||||||
name: 'Satoshis per BCH',
|
name: "Satoshis per BCH",
|
||||||
description: 'Used to display amounts in BCH with decimals.',
|
description: "Used to display amounts in BCH with decimals.",
|
||||||
type: 'integer',
|
type: "integer",
|
||||||
value: 100000000,
|
value: 100000000,
|
||||||
},
|
},
|
||||||
tokenDust: {
|
tokenDust: {
|
||||||
name: 'Token Dust Limit',
|
name: "Token Dust Limit",
|
||||||
description: 'Minimal satoshis required for a token-bearing output.',
|
description: "Minimal satoshis required for a token-bearing output.",
|
||||||
type: 'integer',
|
type: "integer",
|
||||||
value: 1000,
|
value: 1000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
variables: {
|
variables: {
|
||||||
amountToWrap: {
|
amountToWrap: {
|
||||||
name: 'Amount to Wrap',
|
name: "Amount to Wrap",
|
||||||
description: 'How much BCH to convert to wBCH (in satoshis).',
|
description: "How much BCH to convert to wBCH (in satoshis).",
|
||||||
type: 'integer',
|
type: "integer",
|
||||||
hint: 'satoshis',
|
hint: "satoshis",
|
||||||
},
|
},
|
||||||
amountToUnwrap: {
|
amountToUnwrap: {
|
||||||
name: 'Amount to Unwrap',
|
name: "Amount to Unwrap",
|
||||||
description: 'How much wBCH to convert back to BCH (in satoshis).',
|
description: "How much wBCH to convert back to BCH (in satoshis).",
|
||||||
type: 'integer',
|
type: "integer",
|
||||||
hint: 'satoshis',
|
hint: "satoshis",
|
||||||
},
|
},
|
||||||
recipientLockingScript: {
|
recipientLockingScript: {
|
||||||
name: 'Destination',
|
name: "Destination",
|
||||||
description: 'Where to receive your BCH or wBCH tokens.',
|
description: "Where to receive your BCH or wBCH tokens.",
|
||||||
type: 'bytes',
|
type: "bytes",
|
||||||
hint: 'lockingScript',
|
hint: "lockingScript",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
icons: [
|
icons: [
|
||||||
{ name: 'wrap', hash: '0000000000000000000000' },
|
{ name: "wrap", hash: "0000000000000000000000" },
|
||||||
{ name: 'unwrap', hash: '0000000000000000000000' },
|
{ name: "unwrap", hash: "0000000000000000000000" },
|
||||||
{ name: 'user', hash: '0000000000000000000000' },
|
{ name: "user", hash: "0000000000000000000000" },
|
||||||
{ name: 'contract', hash: '0000000000000000000000' },
|
{ name: "contract", hash: "0000000000000000000000" },
|
||||||
{ name: 'receive', hash: '0000000000000000000000' },
|
{ name: "receive", hash: "0000000000000000000000" },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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.
|
// The command is a function that returns a promise that resolves to the result of the command.
|
||||||
const clipboardMethods = {
|
const clipboardMethods = {
|
||||||
pbCopy: {
|
pbCopy: {
|
||||||
platform: (platform: string) => platform === 'darwin',
|
platform: (platform: string) => platform === "darwin",
|
||||||
command: async (text: string) => execAsync(`printf '%s' '${text}' | pbcopy`),
|
command: async (text: string) =>
|
||||||
|
execAsync(`printf '%s' '${text}' | pbcopy`),
|
||||||
},
|
},
|
||||||
xclip: {
|
xclip: {
|
||||||
platform: (platform: string) => platform === 'linux',
|
platform: (platform: string) => platform === "linux",
|
||||||
command: async (text: string) => execAsync(`printf '%s' '${text}' | xclip -selection clipboard`),
|
command: async (text: string) =>
|
||||||
|
execAsync(`printf '%s' '${text}' | xclip -selection clipboard`),
|
||||||
},
|
},
|
||||||
xsel: {
|
xsel: {
|
||||||
platform: (platform: string) => platform === 'linux',
|
platform: (platform: string) => platform === "linux",
|
||||||
command: async (text: string) => execAsync(`printf '%s' '${text}' | xsel --clipboard --input`),
|
command: async (text: string) =>
|
||||||
|
execAsync(`printf '%s' '${text}' | xsel --clipboard --input`),
|
||||||
},
|
},
|
||||||
ssh: {
|
ssh: {
|
||||||
platform: (platform: string) => platform === 'linux',
|
platform: (platform: string) => platform === "linux",
|
||||||
command: async (text: string) => process.stdout.write(`\x1b]52;c;${Buffer.from(text, 'utf-8').toString('base64')}\x07`),
|
command: async (text: string) =>
|
||||||
|
process.stdout.write(
|
||||||
|
`\x1b]52;c;${Buffer.from(text, "utf-8").toString("base64")}\x07`,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
clip: {
|
clip: {
|
||||||
platform: (platform: string) => platform === 'windows',
|
platform: (platform: string) => platform === "windows",
|
||||||
command: async (text: string) => execAsync(`echo|set /p="${text}" | clip`),
|
command: async (text: string) => execAsync(`echo|set /p="${text}" | clip`),
|
||||||
},
|
},
|
||||||
clipboardy: {
|
clipboardy: {
|
||||||
platform: (platform: string) => platform === 'windows',
|
platform: (platform: string) => platform === "windows",
|
||||||
command: async (text: string) => clipboardy.writeSync(text),
|
command: async (text: string) => clipboardy.writeSync(text),
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to copy text to clipboard using multiple methods.
|
* Attempts to copy text to clipboard using multiple methods.
|
||||||
@@ -51,7 +57,9 @@ export async function copyToClipboard(text: string): Promise<void> {
|
|||||||
// Escape the text for shell commands
|
// Escape the text for shell commands
|
||||||
const escapedText = text.replace(/'/g, "'\\''");
|
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[] = [];
|
const errors: Error[] = [];
|
||||||
|
|
||||||
@@ -63,7 +71,7 @@ export async function copyToClipboard(text: string): Promise<void> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
} catch(error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
errors.push(error);
|
errors.push(error);
|
||||||
}
|
}
|
||||||
@@ -71,5 +79,7 @@ export async function copyToClipboard(text: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// All methods failed
|
// 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")}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,8 +160,7 @@ export function listDirectoryEntries(
|
|||||||
entries: [...entries, ...directories, ...files],
|
entries: [...entries, ...directories, ...files],
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message =
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
error instanceof Error ? error.message : String(error);
|
|
||||||
return {
|
return {
|
||||||
entries: [],
|
entries: [],
|
||||||
error: `Unable to read directory: ${message}`,
|
error: `Unable to read directory: ${message}`,
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export function buildHistoryDisplayRows(
|
|||||||
type: "history_output",
|
type: "history_output",
|
||||||
label: output.outpoint
|
label: output.outpoint
|
||||||
? `${output.outpoint.txid}:${output.outpoint.index}`
|
? `${output.outpoint.txid}:${output.outpoint.index}`
|
||||||
: output.outputIdentifier ?? "Output",
|
: (output.outputIdentifier ?? "Output"),
|
||||||
description: `${item.template} | ${roles} | ${output.description}`,
|
description: `${item.template} | ${roles} | ${output.description}`,
|
||||||
timestamp: item.createdAtTimestamp,
|
timestamp: item.createdAtTimestamp,
|
||||||
isNested: false,
|
isNested: false,
|
||||||
@@ -96,7 +96,7 @@ export function buildHistoryDisplayRows(
|
|||||||
type: "history_output",
|
type: "history_output",
|
||||||
label: output.outpoint
|
label: output.outpoint
|
||||||
? `${output.outpoint.txid}:${output.outpoint.index}`
|
? `${output.outpoint.txid}:${output.outpoint.index}`
|
||||||
: output.outputIdentifier ?? "Output",
|
: (output.outputIdentifier ?? "Output"),
|
||||||
description: output.description,
|
description: output.description,
|
||||||
isNested: true,
|
isNested: true,
|
||||||
valueSatoshis: output.valueSatoshis,
|
valueSatoshis: output.valueSatoshis,
|
||||||
|
|||||||
@@ -65,8 +65,18 @@ export const roleRequiresInputs = (
|
|||||||
|
|
||||||
const actionRole = action.roles?.[roleIdentifier];
|
const actionRole = action.roles?.[roleIdentifier];
|
||||||
const actionRequirements = action.requirements;
|
const actionRequirements = action.requirements;
|
||||||
const actionRoleRequirements = actionRole && actionRequirements && actionRequirements.participants?.find((participant) => participant.role === roleIdentifier);
|
const actionRoleRequirements =
|
||||||
const roleSlotsMin = actionRoleRequirements && actionRoleRequirements.slots && actionRoleRequirements.slots.min > 0 ? actionRoleRequirements.slots.min : 0;
|
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;
|
if (roleSlotsMin > 0) return true;
|
||||||
|
|
||||||
const transactionIdentifier = action.transaction;
|
const transactionIdentifier = action.transaction;
|
||||||
@@ -78,7 +88,6 @@ export const roleRequiresInputs = (
|
|||||||
return (roleInputs?.length ?? 0) > 0;
|
return (roleInputs?.length ?? 0) > 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const getTransactionOutputIdentifier = (
|
export const getTransactionOutputIdentifier = (
|
||||||
output: XOTemplateTransactionOutput,
|
output: XOTemplateTransactionOutput,
|
||||||
): string | undefined => {
|
): string | undefined => {
|
||||||
@@ -136,7 +145,8 @@ export const resolveProvidedLockingBytecodeHex = (
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lockingScriptDefinition = template.lockingScripts?.[outputDefinition.lockingScript];
|
const lockingScriptDefinition =
|
||||||
|
template.lockingScripts?.[outputDefinition.lockingScript];
|
||||||
const scriptIdentifier = lockingScriptDefinition?.lockingBytecode;
|
const scriptIdentifier = lockingScriptDefinition?.lockingBytecode;
|
||||||
if (!scriptIdentifier) return undefined;
|
if (!scriptIdentifier) return undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -71,12 +71,7 @@ function resolveTemplateModuleLoaderPath(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** TypeScript extensions that require tsx to evaluate the template module. */
|
/** TypeScript extensions that require tsx to evaluate the template module. */
|
||||||
const TYPESCRIPT_TEMPLATE_EXTENSIONS = new Set([
|
const TYPESCRIPT_TEMPLATE_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts"]);
|
||||||
".ts",
|
|
||||||
".tsx",
|
|
||||||
".mts",
|
|
||||||
".cts",
|
|
||||||
]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads a TS/JS template module in an isolated child process.
|
* Loads a TS/JS template module in an isolated child process.
|
||||||
@@ -155,7 +150,9 @@ async function loadTemplateModuleViaChildProcess(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (stdout.trim().length === 0) {
|
if (stdout.trim().length === 0) {
|
||||||
reject(new TemplateLoadError("Template module loader returned no output."));
|
reject(
|
||||||
|
new TemplateLoadError("Template module loader returned no output."),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +171,9 @@ export async function loadTemplateFromFile(filePath: string): Promise<string> {
|
|||||||
const absolutePath = path.resolve(filePath);
|
const absolutePath = path.resolve(filePath);
|
||||||
|
|
||||||
if (!fs.existsSync(absolutePath)) {
|
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();
|
const extension = path.extname(absolutePath).toLowerCase();
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import { basename, isAbsolute, join, resolve } from "node:path";
|
|||||||
* Base config directory. Created on first access.
|
* Base config directory. Created on first access.
|
||||||
*/
|
*/
|
||||||
export function getConfigDir(): string {
|
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 });
|
mkdirSync(dir, { recursive: true });
|
||||||
return dir;
|
return dir;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@
|
|||||||
* Returns true when `value` looks like an XOTemplate object (pre-schema check).
|
* Returns true when `value` looks like an XOTemplate object (pre-schema check).
|
||||||
* Used only to pick the correct export before {@link parseTemplate} validates fully.
|
* Used only to pick the correct export before {@link parseTemplate} validates fully.
|
||||||
*/
|
*/
|
||||||
export function isTemplateLike(value: unknown): value is Record<string, unknown> {
|
export function isTemplateLike(
|
||||||
|
value: unknown,
|
||||||
|
): value is Record<string, unknown> {
|
||||||
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { EventEmitter } from '../event-emitter.js';
|
import { EventEmitter } from "../event-emitter.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Events emitted by our Rates Adapters
|
* Events emitted by our Rates Adapters
|
||||||
@@ -44,14 +44,15 @@ export abstract class BaseRates<
|
|||||||
BCH: 8,
|
BCH: 8,
|
||||||
USD: 2,
|
USD: 2,
|
||||||
};
|
};
|
||||||
const minimumFractionDigits = minimumFractionDigitsMap[normalizedCurrency] ?? 2;
|
const minimumFractionDigits =
|
||||||
|
minimumFractionDigitsMap[normalizedCurrency] ?? 2;
|
||||||
const maximumFractionDigits = Math.max(minimumFractionDigits, 8);
|
const maximumFractionDigits = Math.max(minimumFractionDigits, 8);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const formatter = new Intl.NumberFormat('en-US', {
|
const formatter = new Intl.NumberFormat("en-US", {
|
||||||
style: 'currency',
|
style: "currency",
|
||||||
currency: normalizedCurrency,
|
currency: normalizedCurrency,
|
||||||
currencyDisplay: 'narrowSymbol',
|
currencyDisplay: "narrowSymbol",
|
||||||
minimumFractionDigits,
|
minimumFractionDigits,
|
||||||
maximumFractionDigits,
|
maximumFractionDigits,
|
||||||
});
|
});
|
||||||
@@ -61,7 +62,7 @@ export abstract class BaseRates<
|
|||||||
// Some numerator symbols from oracle pairs (e.g. DOGE/BCH) are not ISO-4217
|
// 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.
|
// fiat currency codes, so Intl currency formatting will throw a RangeError.
|
||||||
// In that case we still return a human-readable formatted value.
|
// 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,
|
minimumFractionDigits,
|
||||||
maximumFractionDigits,
|
maximumFractionDigits,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import {
|
|||||||
OracleMetadataMessage,
|
OracleMetadataMessage,
|
||||||
OraclePriceMessage,
|
OraclePriceMessage,
|
||||||
type OracleMetadataMap,
|
type OracleMetadataMap,
|
||||||
} from '@generalprotocols/oracle-client';
|
} from "@generalprotocols/oracle-client";
|
||||||
|
|
||||||
import { type RatesEventMap, BaseRates } from './base-rates.js';
|
import { type RatesEventMap, BaseRates } from "./base-rates.js";
|
||||||
import { type OffCallback } from '../event-emitter.js';
|
import { type OffCallback } from "../event-emitter.js";
|
||||||
import { SettingsService } from '../../services/settings.js';
|
import { SettingsService } from "../../services/settings.js";
|
||||||
|
|
||||||
// Add the Oracle Price Message to our Events for this Adapter.
|
// Add the Oracle Price Message to our Events for this Adapter.
|
||||||
export type RatesOracleEventMap = RatesEventMap & {
|
export type RatesOracleEventMap = RatesEventMap & {
|
||||||
@@ -42,7 +42,7 @@ export class RatesOracle extends BaseRates<RatesOracleEventMap> {
|
|||||||
|
|
||||||
private started: boolean = false;
|
private started: boolean = false;
|
||||||
private targetNumeratorUnitCode: string;
|
private targetNumeratorUnitCode: string;
|
||||||
private targetDenominatorUnitCode: string = 'BCH';
|
private targetDenominatorUnitCode: string = "BCH";
|
||||||
private unsubscribeFromSettings: OffCallback | null = null;
|
private unsubscribeFromSettings: OffCallback | null = null;
|
||||||
|
|
||||||
public constructor(client: OracleClient, settings: SettingsService) {
|
public constructor(client: OracleClient, settings: SettingsService) {
|
||||||
@@ -63,7 +63,7 @@ export class RatesOracle extends BaseRates<RatesOracleEventMap> {
|
|||||||
}
|
}
|
||||||
this.started = true;
|
this.started = true;
|
||||||
this.unsubscribeFromSettings = this.settings.on(
|
this.unsubscribeFromSettings = this.settings.on(
|
||||||
'settings-updated',
|
"settings-updated",
|
||||||
this.handleSettingsUpdated.bind(this),
|
this.handleSettingsUpdated.bind(this),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -150,7 +150,11 @@ export class RatesOracle extends BaseRates<RatesOracleEventMap> {
|
|||||||
this.handlePriceMessage(message);
|
this.handlePriceMessage(message);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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<RatesOracleEventMap> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceNumeratorUnitCode = oracle.SOURCE_NUMERATOR_UNIT_CODE.toUpperCase();
|
const sourceNumeratorUnitCode =
|
||||||
const sourceDenominatorUnitCode = oracle.SOURCE_DENOMINATOR_UNIT_CODE.toUpperCase();
|
oracle.SOURCE_NUMERATOR_UNIT_CODE.toUpperCase();
|
||||||
|
const sourceDenominatorUnitCode =
|
||||||
|
oracle.SOURCE_DENOMINATOR_UNIT_CODE.toUpperCase();
|
||||||
|
|
||||||
// Only emit the pair currently selected in settings.
|
// Only emit the pair currently selected in settings.
|
||||||
if (
|
if (
|
||||||
@@ -197,7 +203,7 @@ export class RatesOracle extends BaseRates<RatesOracleEventMap> {
|
|||||||
// Scale the price
|
// Scale the price
|
||||||
const priceValue = message.priceValue / oracle.ATTESTATION_SCALING;
|
const priceValue = message.priceValue / oracle.ATTESTATION_SCALING;
|
||||||
|
|
||||||
this.emit('rateUpdated', {
|
this.emit("rateUpdated", {
|
||||||
numeratorUnitCode: sourceNumeratorUnitCode,
|
numeratorUnitCode: sourceNumeratorUnitCode,
|
||||||
denominatorUnitCode: sourceDenominatorUnitCode,
|
denominatorUnitCode: sourceDenominatorUnitCode,
|
||||||
price: priceValue,
|
price: priceValue,
|
||||||
@@ -208,13 +214,11 @@ export class RatesOracle extends BaseRates<RatesOracleEventMap> {
|
|||||||
/**
|
/**
|
||||||
* Tracks updates to settings and switches the actively emitted fiat pair.
|
* Tracks updates to settings and switches the actively emitted fiat pair.
|
||||||
*/
|
*/
|
||||||
private handleSettingsUpdated(
|
private handleSettingsUpdated(event: {
|
||||||
event: {
|
key: "currency" | "default-mnemonic";
|
||||||
key: 'currency' | 'default-mnemonic';
|
value: string | undefined;
|
||||||
value: string | undefined;
|
}) {
|
||||||
},
|
if (event.key !== "currency" || !event.value) {
|
||||||
) {
|
|
||||||
if (event.key !== 'currency' || !event.value) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +227,7 @@ export class RatesOracle extends BaseRates<RatesOracleEventMap> {
|
|||||||
// Refresh so listeners get the latest value for the new currency quickly.
|
// Refresh so listeners get the latest value for the new currency quickly.
|
||||||
if (this.started) {
|
if (this.started) {
|
||||||
this.refreshPrices().catch((error) => {
|
this.refreshPrices().catch((error) => {
|
||||||
console.error('Error refreshing prices after currency update:', error);
|
console.error("Error refreshing prices after currency update:", error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,7 @@ try {
|
|||||||
const template = pickTemplateExport(loadedModule);
|
const template = pickTemplateExport(loadedModule);
|
||||||
process.stdout.write(serializeTemplate(template as XOTemplate));
|
process.stdout.write(serializeTemplate(template as XOTemplate));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message =
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
error instanceof Error ? error.message : String(error);
|
|
||||||
console.error(`Failed to load template module: ${message}`);
|
console.error(`Failed to load template module: ${message}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -188,13 +188,13 @@ export function getRolesForAction(
|
|||||||
);
|
);
|
||||||
|
|
||||||
return startEntries.map((entry) => {
|
return startEntries.map((entry) => {
|
||||||
const roleDef = template.roles?.[entry.role || ''];
|
const roleDef = template.roles?.[entry.role || ""];
|
||||||
const roleObj = typeof roleDef === "object" ? roleDef : null;
|
const roleObj = typeof roleDef === "object" ? roleDef : null;
|
||||||
|
|
||||||
// TODO: This is ugly. Lot of conditionals. Need to take a much closer look at this.
|
// TODO: This is ugly. Lot of conditionals. Need to take a much closer look at this.
|
||||||
return {
|
return {
|
||||||
roleId: entry.role || '',
|
roleId: entry.role || "",
|
||||||
name: roleObj?.name || entry.role || '',
|
name: roleObj?.name || entry.role || "",
|
||||||
description: roleObj?.description,
|
description: roleObj?.description,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ export type UnspentOutputMetadata = {
|
|||||||
outputIdentifier?: string;
|
outputIdentifier?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UnspentOutputWithMetadata = UnspentOutputData & UnspentOutputMetadata;
|
export type UnspentOutputWithMetadata = UnspentOutputData &
|
||||||
|
UnspentOutputMetadata;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a lookup map from script hash to its stored metadata.
|
* Builds a lookup map from script hash to its stored metadata.
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ describe("shell completions", () => {
|
|||||||
test("uses shell-native mnemonic completion in fish", () => {
|
test("uses shell-native mnemonic completion in fish", () => {
|
||||||
const completions = generateFishCompletions("xo-cli");
|
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).toContain("(__xo_cli_complete_mnemonics)");
|
||||||
expect(completions).not.toContain("(__xo_cli_complete_dynamic mnemonics)");
|
expect(completions).not.toContain("(__xo_cli_complete_dynamic mnemonics)");
|
||||||
});
|
});
|
||||||
@@ -68,9 +68,9 @@ describe("shell completions", () => {
|
|||||||
|
|
||||||
const contents = readFileSync(configFile, "utf8");
|
const contents = readFileSync(configFile, "utf8");
|
||||||
expect(contents.match(/XO_CONFIG_DIR/g)).toHaveLength(2);
|
expect(contents.match(/XO_CONFIG_DIR/g)).toHaveLength(2);
|
||||||
expect(contents.match(/eval "\$\(xo-cli completions bash\)"/g)).toHaveLength(
|
expect(
|
||||||
1,
|
contents.match(/eval "\$\(xo-cli completions bash\)"/g),
|
||||||
);
|
).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("adds a missing default without duplicating an existing loader", () => {
|
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);
|
expect(installCompletions("bash", "xo-cli", configFile)).toBe(true);
|
||||||
|
|
||||||
const contents = readFileSync(configFile, "utf8");
|
const contents = readFileSync(configFile, "utf8");
|
||||||
expect(contents.match(/eval "\$\(xo-cli completions bash\)"/g)).toHaveLength(
|
expect(
|
||||||
1,
|
contents.match(/eval "\$\(xo-cli completions bash\)"/g),
|
||||||
);
|
).toHaveLength(1);
|
||||||
expect(contents).toContain(
|
expect(contents).toContain(
|
||||||
'export XO_CONFIG_DIR="${XO_CONFIG_DIR:-$HOME/.config/xo-cli}"',
|
'export XO_CONFIG_DIR="${XO_CONFIG_DIR:-$HOME/.config/xo-cli}"',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("preserves an existing custom config directory assignment", () => {
|
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);
|
expect(installCompletions("zsh", "xo-cli", configFile)).toBe(true);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
currency: string;
|
||||||
"default-mnemonic"?: string;
|
"default-mnemonic"?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ const testCases: TestCase[] = [
|
|||||||
inputs: ["export", p2pkhTemplateIdentifier],
|
inputs: ["export", p2pkhTemplateIdentifier],
|
||||||
shouldThrow: false,
|
shouldThrow: false,
|
||||||
expectedData: {},
|
expectedData: {},
|
||||||
logs: [{ out: "\"name\":\"Wallet (P2PKH)\"" }],
|
logs: [{ out: '"name":"Wallet (P2PKH)"' }],
|
||||||
},
|
},
|
||||||
// Error cases - subcommand
|
// Error cases - subcommand
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -113,7 +113,9 @@ describe("mnemonic utilities", () => {
|
|||||||
|
|
||||||
// Due to some weird MacOS behavior we need to use realpathSync to get the correct path
|
// 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()}`
|
// 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
|
// Compare to the expected path
|
||||||
expect(resolved).toBe(expectedPath);
|
expect(resolved).toBe(expectedPath);
|
||||||
|
|||||||
@@ -159,7 +159,9 @@ export const createMockEngine = async (seed: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const createMockAppService = async (engine: Engine) => {
|
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");
|
settings.setCurrency("USD");
|
||||||
|
|
||||||
const storage = await InMemoryStorage.create();
|
const storage = await InMemoryStorage.create();
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ export class MockRatesService extends BaseRates {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRate(numeratorUnitCode: string, denominatorUnitCode: string): Promise<number> {
|
async getRate(
|
||||||
|
numeratorUnitCode: string,
|
||||||
|
denominatorUnitCode: string,
|
||||||
|
): Promise<number> {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,11 @@
|
|||||||
import { expect, test, describe, beforeEach, afterEach } from "vitest";
|
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 { homedir, tmpdir } from "node:os";
|
||||||
import path from "node:path";
|
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
|
// 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()}`
|
// 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
|
// Compare to the expected path
|
||||||
expect(resolved).toBe(expectedPath);
|
expect(resolved).toBe(expectedPath);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ describe("formatDialogMessageLines", () => {
|
|||||||
|
|
||||||
test("breaks long dot-separated paths at segment boundaries", () => {
|
test("breaks long dot-separated paths at segment boundaries", () => {
|
||||||
const line =
|
const line =
|
||||||
"- actions.requestFungibleTokens.roles.receiver.requirements: Unrecognized key: \"generate\"";
|
'- actions.requestFungibleTokens.roles.receiver.requirements: Unrecognized key: "generate"';
|
||||||
const lines = formatDialogMessageLines(line, 56);
|
const lines = formatDialogMessageLines(line, 56);
|
||||||
|
|
||||||
expect(lines.length).toBeGreaterThan(1);
|
expect(lines.length).toBeGreaterThan(1);
|
||||||
|
|||||||
Reference in New Issue
Block a user