Compare commits
7 Commits
53ad7b729e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f2e515d72 | |||
| 7ffb5c44b5 | |||
| f978d740fe | |||
| 6196d33b2a | |||
| ccfaf3fd70 | |||
| 531e53d2ae | |||
| b708c8c1f8 |
165
package-lock.json
generated
165
package-lock.json
generated
@@ -12,11 +12,11 @@
|
|||||||
"@bitauth/libauth": "^3.0.0",
|
"@bitauth/libauth": "^3.0.0",
|
||||||
"@electrum-cash/protocol": "^2.3.1",
|
"@electrum-cash/protocol": "^2.3.1",
|
||||||
"@generalprotocols/oracle-client": "^0.0.1-development.11945476152",
|
"@generalprotocols/oracle-client": "^0.0.1-development.11945476152",
|
||||||
"@xo-cash/crypto": "file:../crypto",
|
"@xo-cash/crypto": "^0.0.1",
|
||||||
"@xo-cash/engine": "file:../engine",
|
"@xo-cash/engine": "file:../engine",
|
||||||
"@xo-cash/state": "file:../state",
|
"@xo-cash/state": "file:../state",
|
||||||
"@xo-cash/templates": "file:../templates",
|
"@xo-cash/templates": "^0.0.1",
|
||||||
"@xo-cash/types": "file:../types",
|
"@xo-cash/types": "^0.0.1",
|
||||||
"better-sqlite3": "^12.6.2",
|
"better-sqlite3": "^12.6.2",
|
||||||
"clipboardy": "^5.1.0",
|
"clipboardy": "^5.1.0",
|
||||||
"ink": "^6.6.0",
|
"ink": "^6.6.0",
|
||||||
@@ -41,38 +41,6 @@
|
|||||||
"vitest": "^4.1.2"
|
"vitest": "^4.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"../crypto": {
|
|
||||||
"name": "@xo-cash/crypto",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@bitauth/libauth": "^3.1.0-next.8",
|
|
||||||
"@xo-cash/primitives": "0.0.1",
|
|
||||||
"@xo-cash/types": "0.0.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@chalp/eslint-airbnb": "^1.3.0",
|
|
||||||
"@generalprotocols/cspell-dictionary": "^1.0.1",
|
|
||||||
"@stylistic/eslint-plugin": "^5.7.0",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^8.53.1",
|
|
||||||
"@typescript-eslint/parser": "^8.53.1",
|
|
||||||
"@vitest/coverage-v8": "^4.0.17",
|
|
||||||
"@viz-kit/esbuild-analyzer": "^1.0.0",
|
|
||||||
"@xo-cash/eslint-config": "1.0.1",
|
|
||||||
"cspell": "^9.6.0",
|
|
||||||
"eslint": "^9.39.2",
|
|
||||||
"prettier": "^3.6.2",
|
|
||||||
"tsdown": "^0.20.0-beta.4",
|
|
||||||
"typedoc": "^0.28.16",
|
|
||||||
"typedoc-plugin-coverage": "^4.0.2",
|
|
||||||
"typescript": "^5.3.2",
|
|
||||||
"typescript-eslint": "^8.53.1",
|
|
||||||
"vitest": "^4.0.17"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"../engine": {
|
"../engine": {
|
||||||
"name": "@xo-cash/engine",
|
"name": "@xo-cash/engine",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
@@ -85,7 +53,8 @@
|
|||||||
"@electrum-cash/servers": "^3.1.0",
|
"@electrum-cash/servers": "^3.1.0",
|
||||||
"@xo-cash/crypto": "0.0.1",
|
"@xo-cash/crypto": "0.0.1",
|
||||||
"@xo-cash/primitives": "0.0.1",
|
"@xo-cash/primitives": "0.0.1",
|
||||||
"@xo-cash/state": "0.0.1",
|
"@xo-cash/state": "0.0.2",
|
||||||
|
"@xo-cash/templates": "0.0.1",
|
||||||
"@xo-cash/types": "0.0.1",
|
"@xo-cash/types": "0.0.1",
|
||||||
"@xo-cash/utils": "0.0.1",
|
"@xo-cash/utils": "0.0.1",
|
||||||
"eventemitter3": "^5.0.1"
|
"eventemitter3": "^5.0.1"
|
||||||
@@ -108,14 +77,11 @@
|
|||||||
"typescript": "^5.3.2",
|
"typescript": "^5.3.2",
|
||||||
"typescript-eslint": "^8.53.1",
|
"typescript-eslint": "^8.53.1",
|
||||||
"vitest": "^4.0.17"
|
"vitest": "^4.0.17"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"../state": {
|
"../state": {
|
||||||
"name": "@xo-cash/state",
|
"name": "@xo-cash/state",
|
||||||
"version": "0.0.1",
|
"version": "0.0.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bitauth/libauth": "^3.1.0-next.8",
|
"@bitauth/libauth": "^3.1.0-next.8",
|
||||||
@@ -147,60 +113,6 @@
|
|||||||
"vitest": "^4.0.17"
|
"vitest": "^4.0.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"../templates": {
|
|
||||||
"name": "@xo-cash/templates",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@xo-cash/types": "0.0.1-development.13504604083"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@chalp/eslint-airbnb": "^1.3.0",
|
|
||||||
"@generalprotocols/cspell-dictionary": "^1.0.1",
|
|
||||||
"@stylistic/eslint-plugin": "^5.7.0",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^8.53.1",
|
|
||||||
"@typescript-eslint/parser": "^8.53.1",
|
|
||||||
"@vitest/coverage-v8": "^4.0.17",
|
|
||||||
"@viz-kit/esbuild-analyzer": "^1.0.0",
|
|
||||||
"@xo-cash/eslint-config": "1.0.1",
|
|
||||||
"cspell": "^9.6.0",
|
|
||||||
"eslint": "^9.39.2",
|
|
||||||
"prettier": "^3.6.2",
|
|
||||||
"tsdown": "^0.20.0-beta.4",
|
|
||||||
"typedoc": "^0.28.16",
|
|
||||||
"typedoc-plugin-coverage": "^4.0.2",
|
|
||||||
"typescript": "^5.3.2",
|
|
||||||
"typescript-eslint": "^8.53.1",
|
|
||||||
"vitest": "^4.0.17"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"../types": {
|
|
||||||
"name": "@xo-cash/types",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@bitauth/libauth": "^3.1.0-next.8"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@chalp/eslint-airbnb": "^1.3.0",
|
|
||||||
"@generalprotocols/cspell-dictionary": "^1.0.1",
|
|
||||||
"@stylistic/eslint-plugin": "^5.7.0",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^8.53.1",
|
|
||||||
"@typescript-eslint/parser": "^8.53.1",
|
|
||||||
"@vitest/coverage-v8": "^4.0.17",
|
|
||||||
"@viz-kit/esbuild-analyzer": "^1.0.0",
|
|
||||||
"@xo-cash/eslint-config": "1.0.1",
|
|
||||||
"cspell": "^9.6.0",
|
|
||||||
"eslint": "^9.39.2",
|
|
||||||
"prettier": "^3.6.2",
|
|
||||||
"tsdown": "^0.20.0-beta.4",
|
|
||||||
"typedoc": "^0.28.16",
|
|
||||||
"typedoc-plugin-coverage": "^4.0.2",
|
|
||||||
"typescript": "^5.3.2",
|
|
||||||
"typescript-eslint": "^8.53.1",
|
|
||||||
"vitest": "^4.0.17"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@alcalzone/ansi-tokenize": {
|
"node_modules/@alcalzone/ansi-tokenize": {
|
||||||
"version": "0.2.4",
|
"version": "0.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.2.4.tgz",
|
||||||
@@ -971,24 +883,77 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@xo-cash/crypto": {
|
"node_modules/@xo-cash/crypto": {
|
||||||
"resolved": "../crypto",
|
"version": "0.0.1",
|
||||||
"link": true
|
"resolved": "https://registry.npmjs.org/@xo-cash/crypto/-/crypto-0.0.1.tgz",
|
||||||
|
"integrity": "sha512-ZIa9MHAVCBJqo5uxyx/Tx/jTSyyJw1cfYfI48gEHqBIl5wyyxiZDx4eZvVWSr8uKgS5Tm3FXUkKQybvk5QGRIQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@bitauth/libauth": "^3.1.0-next.8",
|
||||||
|
"@xo-cash/primitives": "0.0.1",
|
||||||
|
"@xo-cash/types": "0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@xo-cash/crypto/node_modules/@bitauth/libauth": {
|
||||||
|
"version": "3.1.0-next.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@bitauth/libauth/-/libauth-3.1.0-next.8.tgz",
|
||||||
|
"integrity": "sha512-Pm+Ju+YP3JeBLLTiVrBnia2wwE4G17r4XqpvPRMcklElJTe8J6x3JgKRg1by0Xm3ZY6UFxACkEAoSA+x419/zA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@xo-cash/engine": {
|
"node_modules/@xo-cash/engine": {
|
||||||
"resolved": "../engine",
|
"resolved": "../engine",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@xo-cash/primitives": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xo-cash/primitives/-/primitives-0.0.1.tgz",
|
||||||
|
"integrity": "sha512-medxVK9Sawj7oIDhWvTjTgzwf6BjGao6CXtQYJOUFi6NOO1eclb1PDjEmkG/4NeK3v7LQIN8QS60mTAGyS9FXg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@bitauth/libauth": "^3.1.0-next.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@xo-cash/primitives/node_modules/@bitauth/libauth": {
|
||||||
|
"version": "3.1.0-next.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@bitauth/libauth/-/libauth-3.1.0-next.8.tgz",
|
||||||
|
"integrity": "sha512-Pm+Ju+YP3JeBLLTiVrBnia2wwE4G17r4XqpvPRMcklElJTe8J6x3JgKRg1by0Xm3ZY6UFxACkEAoSA+x419/zA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@xo-cash/state": {
|
"node_modules/@xo-cash/state": {
|
||||||
"resolved": "../state",
|
"resolved": "../state",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
"node_modules/@xo-cash/templates": {
|
"node_modules/@xo-cash/templates": {
|
||||||
"resolved": "../templates",
|
"version": "0.0.1",
|
||||||
"link": true
|
"resolved": "https://registry.npmjs.org/@xo-cash/templates/-/templates-0.0.1.tgz",
|
||||||
|
"integrity": "sha512-v5f0YeH9Bw6lNThdE0fI878T4L2jbM8RI1quxdKxnvqHn9hu2jzebqvveEB2TfJWG3sP1GpE1go0Yn87R4sXfw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@xo-cash/types": "0.0.1"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@xo-cash/types": {
|
"node_modules/@xo-cash/types": {
|
||||||
"resolved": "../types",
|
"version": "0.0.1",
|
||||||
"link": true
|
"resolved": "https://registry.npmjs.org/@xo-cash/types/-/types-0.0.1.tgz",
|
||||||
|
"integrity": "sha512-BMwh2Y9+LqnTXYmdA7Nxi1NuK+AcsNWFoFGJVAvuY5TBfsbNIzWppjmrI2fAyj/RlSE3tATMxam+6CJb3RnDIA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@bitauth/libauth": "^3.1.0-next.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@xo-cash/types/node_modules/@bitauth/libauth": {
|
||||||
|
"version": "3.1.0-next.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@bitauth/libauth/-/libauth-3.1.0-next.8.tgz",
|
||||||
|
"integrity": "sha512-Pm+Ju+YP3JeBLLTiVrBnia2wwE4G17r4XqpvPRMcklElJTe8J6x3JgKRg1by0Xm3ZY6UFxACkEAoSA+x419/zA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ansi-escapes": {
|
"node_modules/ansi-escapes": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
|
|||||||
12
package.json
12
package.json
@@ -19,7 +19,11 @@
|
|||||||
"nuke": "tsx scripts/rm-dbs.ts",
|
"nuke": "tsx scripts/rm-dbs.ts",
|
||||||
"nuke:dry": "tsx scripts/rm-dbs.ts --dry",
|
"nuke:dry": "tsx scripts/rm-dbs.ts --dry",
|
||||||
"format": "prettier --write \"**/*.{js,ts,md,json}\" --ignore-path .gitignore",
|
"format": "prettier --write \"**/*.{js,ts,md,json}\" --ignore-path .gitignore",
|
||||||
"format:check": "prettier --check ."
|
"format:check": "prettier --check .",
|
||||||
|
"autocomplete:install": "node dist/cli/index.js completions bash --install",
|
||||||
|
"autocomplete:install:bash": "node dist/cli/index.js completions bash --install",
|
||||||
|
"autocomplete:install:zsh": "node dist/cli/index.js completions zsh --install",
|
||||||
|
"autocomplete:install:fish": "node dist/cli/index.js completions fish --install"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"crypto",
|
"crypto",
|
||||||
@@ -34,11 +38,11 @@
|
|||||||
"@bitauth/libauth": "^3.0.0",
|
"@bitauth/libauth": "^3.0.0",
|
||||||
"@electrum-cash/protocol": "^2.3.1",
|
"@electrum-cash/protocol": "^2.3.1",
|
||||||
"@generalprotocols/oracle-client": "^0.0.1-development.11945476152",
|
"@generalprotocols/oracle-client": "^0.0.1-development.11945476152",
|
||||||
"@xo-cash/crypto": "file:../crypto",
|
"@xo-cash/crypto": "^0.0.1",
|
||||||
"@xo-cash/engine": "file:../engine",
|
"@xo-cash/engine": "file:../engine",
|
||||||
"@xo-cash/state": "file:../state",
|
"@xo-cash/state": "file:../state",
|
||||||
"@xo-cash/templates": "file:../templates",
|
"@xo-cash/templates": "^0.0.1",
|
||||||
"@xo-cash/types": "file:../types",
|
"@xo-cash/types": "^0.0.1",
|
||||||
"better-sqlite3": "^12.6.2",
|
"better-sqlite3": "^12.6.2",
|
||||||
"clipboardy": "^5.1.0",
|
"clipboardy": "^5.1.0",
|
||||||
"ink": "^6.6.0",
|
"ink": "^6.6.0",
|
||||||
|
|||||||
55
readme.md
55
readme.md
@@ -5,23 +5,48 @@
|
|||||||
### 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
|
||||||
mdkir xo-terminal && cd xo-terminal
|
mkdir xo-terminal && cd xo-terminal
|
||||||
|
|
||||||
# Clone the Engine Repo
|
# ----- Start Engine Setup -----
|
||||||
git clone git@gitlab.com:GeneralProtocols/xo/engine.git
|
# Clone the Engine Repo (Note, this uses harvey's fork of the engine repo to access the cli-test branch)
|
||||||
|
git clone git@gitlab.com:Harvmaster/engine.git
|
||||||
|
|
||||||
# Move into teh engine directory
|
# Move into teh engine directory
|
||||||
cd engine
|
cd engine
|
||||||
|
|
||||||
|
# Checkout the cli-test branch
|
||||||
|
git checkout cli-test
|
||||||
|
|
||||||
# Install the dependencies
|
# Install the dependencies
|
||||||
npm ci
|
npm ci
|
||||||
|
|
||||||
# Build the engine
|
# Build the engine
|
||||||
npm run build
|
npm run build
|
||||||
|
# ----- End Engine Setup -----
|
||||||
|
|
||||||
# Move back to the top level directory
|
# Move back to the top level directory
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
|
# ----- Start State Setup -----
|
||||||
|
# Clone the State Repo
|
||||||
|
git clone git@gitlab.com:Harvmaster/state.git
|
||||||
|
|
||||||
|
# Move into the state directory
|
||||||
|
cd state
|
||||||
|
|
||||||
|
git checkout in-memory-adapter
|
||||||
|
|
||||||
|
# Install the dependencies
|
||||||
|
npm ci
|
||||||
|
|
||||||
|
# Build the state
|
||||||
|
npm run build
|
||||||
|
# ----- End State Setup -----
|
||||||
|
|
||||||
|
# Move back to the top level directory
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# ----- Start CLI Setup -----
|
||||||
# Clone the CLI Repo
|
# Clone the CLI Repo
|
||||||
git clone git@git.harvmaster.com:Harvmaster/xo-cli.git
|
git clone git@git.harvmaster.com:Harvmaster/xo-cli.git
|
||||||
|
|
||||||
@@ -33,6 +58,13 @@ npm ci
|
|||||||
|
|
||||||
# Build the cli
|
# Build the cli
|
||||||
npm run build
|
npm run build
|
||||||
|
# ----- End CLI Setup -----
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run TUI in dev mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install globally
|
### Install globally
|
||||||
@@ -42,6 +74,23 @@ npm run build
|
|||||||
npm install -g .
|
npm install -g .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Install autocomplete completions (From the xo-cli directory)
|
||||||
|
|
||||||
|
#### Install for bash
|
||||||
|
```bash
|
||||||
|
npm run autocomplete:install:bash
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Install for zsh
|
||||||
|
```bash
|
||||||
|
npm run autocomplete:install:zsh
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Install for fish
|
||||||
|
```bash
|
||||||
|
npm run autocomplete:install:fish
|
||||||
|
```
|
||||||
|
|
||||||
### Run the CLI
|
### 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)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { EventEmitter } from "../utils/event-emitter.js";
|
|||||||
import { createHash } from "crypto";
|
import { createHash } from "crypto";
|
||||||
import { p2pkhTemplate } from "@xo-cash/templates";
|
import { p2pkhTemplate } from "@xo-cash/templates";
|
||||||
import { hexToBin } from "@bitauth/libauth";
|
import { hexToBin } from "@bitauth/libauth";
|
||||||
|
import { parseTemplate } from "@xo-cash/engine";
|
||||||
|
|
||||||
export type AppEventMap = {
|
export type AppEventMap = {
|
||||||
"invitation-added": Invitation;
|
"invitation-added": Invitation;
|
||||||
@@ -95,7 +96,7 @@ export class AppService extends EventEmitter<AppEventMap> {
|
|||||||
// To my knowledge, this doesnt generate any lockscript, so discovery of funds will not work automatically.
|
// To my knowledge, this doesnt generate any lockscript, so discovery of funds will not work automatically.
|
||||||
// TODO: Add discovery for funds in the first index? Or until we return 0 TXs?
|
// TODO: Add discovery for funds in the first index? Or until we return 0 TXs?
|
||||||
await engine.setDefaultLockingParameters(
|
await engine.setDefaultLockingParameters(
|
||||||
generateTemplateIdentifier(p2pkhTemplate),
|
generateTemplateIdentifier(parseTemplate(p2pkhTemplate)),
|
||||||
"receiveOutput",
|
"receiveOutput",
|
||||||
"receiver",
|
"receiver",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { binToHex } from "@bitauth/libauth";
|
import { binToHex } from "@bitauth/libauth";
|
||||||
import { compileCashAssemblyString, type Engine } from "@xo-cash/engine";
|
import {
|
||||||
|
compileCashAssemblyString,
|
||||||
|
type Engine,
|
||||||
|
listInvitationCommitsByEntity,
|
||||||
|
} from "@xo-cash/engine";
|
||||||
import type { UnspentOutputData } from "@xo-cash/state";
|
import type { UnspentOutputData } from "@xo-cash/state";
|
||||||
import type {
|
import type {
|
||||||
XOInvitation,
|
XOInvitation,
|
||||||
@@ -59,6 +63,7 @@ interface InvitationContext {
|
|||||||
invitation: Invitation;
|
invitation: Invitation;
|
||||||
template: XOTemplate | null;
|
template: XOTemplate | null;
|
||||||
variables: Record<string, XOInvitationVariableValue>;
|
variables: Record<string, XOInvitationVariableValue>;
|
||||||
|
walletCommits: XOInvitationCommit[];
|
||||||
walletEntityIdentifier?: string;
|
walletEntityIdentifier?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,12 +78,8 @@ export class HistoryService {
|
|||||||
private invitations: Invitation[],
|
private invitations: Invitation[],
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async extractEntities(invitation: XOInvitation): Promise<string[]> {
|
extractEntities(invitation: XOInvitation): Record<string, XOInvitationCommit[]> {
|
||||||
const entities = new Set<string>();
|
return listInvitationCommitsByEntity(invitation);
|
||||||
for (const commit of invitation.commits) {
|
|
||||||
entities.add(commit.entityIdentifier);
|
|
||||||
}
|
|
||||||
return Array.from(entities);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Entities are currently static per invitation. So, we can try to match the roles to entities by:
|
// Entities are currently static per invitation. So, we can try to match the roles to entities by:
|
||||||
@@ -127,8 +128,6 @@ export class HistoryService {
|
|||||||
|
|
||||||
async getHistory(): Promise<HistoryItem[]> {
|
async getHistory(): Promise<HistoryItem[]> {
|
||||||
const allUtxos = await this.engine.listUnspentOutputsData();
|
const allUtxos = await this.engine.listUnspentOutputsData();
|
||||||
const ownOutpoints = new Set<string>();
|
|
||||||
const ownLockingBytecodes = new Set<string>();
|
|
||||||
const invitationByOrigin = new Map<string, UtxoOriginContext>();
|
const invitationByOrigin = new Map<string, UtxoOriginContext>();
|
||||||
const outpointValueSatoshis = new Map<string, bigint>();
|
const outpointValueSatoshis = new Map<string, bigint>();
|
||||||
|
|
||||||
@@ -137,8 +136,6 @@ export class HistoryService {
|
|||||||
utxo.outpointTransactionHash,
|
utxo.outpointTransactionHash,
|
||||||
utxo.outpointIndex,
|
utxo.outpointIndex,
|
||||||
);
|
);
|
||||||
ownOutpoints.add(outpointKey);
|
|
||||||
ownLockingBytecodes.add(utxo.lockingBytecode);
|
|
||||||
outpointValueSatoshis.set(outpointKey, BigInt(utxo.valueSatoshis));
|
outpointValueSatoshis.set(outpointKey, BigInt(utxo.valueSatoshis));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,15 +145,15 @@ export class HistoryService {
|
|||||||
const template =
|
const template =
|
||||||
(await this.engine.getTemplate(invitation.data.templateIdentifier)) ??
|
(await this.engine.getTemplate(invitation.data.templateIdentifier)) ??
|
||||||
null;
|
null;
|
||||||
const walletEntityIdentifier = this.resolveWalletEntityIdentifier(
|
const walletCommits = await this.getWalletCommitsForInvitation(
|
||||||
invitation,
|
invitation.data,
|
||||||
ownOutpoints,
|
|
||||||
ownLockingBytecodes,
|
|
||||||
);
|
);
|
||||||
|
const walletEntityIdentifier = walletCommits[0]?.entityIdentifier;
|
||||||
contexts.set(invitation.data.invitationIdentifier, {
|
contexts.set(invitation.data.invitationIdentifier, {
|
||||||
invitation,
|
invitation,
|
||||||
template,
|
template,
|
||||||
variables,
|
variables,
|
||||||
|
walletCommits,
|
||||||
walletEntityIdentifier,
|
walletEntityIdentifier,
|
||||||
});
|
});
|
||||||
this.indexInvitationOutputsByUtxoOrigin(invitationByOrigin, invitation);
|
this.indexInvitationOutputsByUtxoOrigin(invitationByOrigin, invitation);
|
||||||
@@ -186,7 +183,6 @@ export class HistoryService {
|
|||||||
const invitationInputs = this.buildWalletInputItemsForInvitation(
|
const invitationInputs = this.buildWalletInputItemsForInvitation(
|
||||||
context,
|
context,
|
||||||
roles[0],
|
roles[0],
|
||||||
invitationOutputs.length > 0,
|
|
||||||
outpointValueSatoshis,
|
outpointValueSatoshis,
|
||||||
);
|
);
|
||||||
const invitationDescription = this.deriveInvitationDescription(
|
const invitationDescription = this.deriveInvitationDescription(
|
||||||
@@ -287,51 +283,25 @@ export class HistoryService {
|
|||||||
return outputs;
|
return outputs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getWalletCommitsForInvitation(
|
||||||
|
invitation: XOInvitation,
|
||||||
|
): Promise<XOInvitationCommit[]> {
|
||||||
|
try {
|
||||||
|
return await this.engine.getOwnCommits(invitation);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private buildWalletInputItemsForInvitation(
|
private buildWalletInputItemsForInvitation(
|
||||||
context: InvitationContext,
|
context: InvitationContext,
|
||||||
walletRole?: string,
|
walletRole?: string,
|
||||||
hasWalletOutputs: boolean = false,
|
|
||||||
outpointValueSatoshis: Map<string, bigint> = new Map(),
|
outpointValueSatoshis: Map<string, bigint> = new Map(),
|
||||||
): HistoryUtxoItem[] {
|
): HistoryUtxoItem[] {
|
||||||
const invitation = context.invitation.data;
|
const invitation = context.invitation.data;
|
||||||
const commits = invitation.commits ?? [];
|
const relevantCommits = context.walletCommits.filter(
|
||||||
const commitsByEntity = context.walletEntityIdentifier
|
|
||||||
? commits.filter(
|
|
||||||
(commit) =>
|
|
||||||
commit.entityIdentifier === context.walletEntityIdentifier,
|
|
||||||
)
|
|
||||||
: [];
|
|
||||||
const commitsByRole = walletRole
|
|
||||||
? commits.filter(
|
|
||||||
(commit) =>
|
|
||||||
this.deriveCommitRoleIdentifier(
|
|
||||||
commit,
|
|
||||||
invitation,
|
|
||||||
context.template,
|
|
||||||
) === walletRole,
|
|
||||||
)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
let relevantCommits = commitsByEntity.filter(
|
|
||||||
(commit) => (commit.data.inputs?.length ?? 0) > 0,
|
(commit) => (commit.data.inputs?.length ?? 0) > 0,
|
||||||
);
|
);
|
||||||
if (relevantCommits.length === 0) {
|
|
||||||
relevantCommits = commitsByRole.filter(
|
|
||||||
(commit) => (commit.data.inputs?.length ?? 0) > 0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (relevantCommits.length === 0 && walletRole === "sender") {
|
|
||||||
relevantCommits = commits.filter(
|
|
||||||
(commit) => (commit.data.inputs?.length ?? 0) > 0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Sender fallback only when no wallet outputs were matched.
|
|
||||||
if (relevantCommits.length === 0 && !hasWalletOutputs) {
|
|
||||||
relevantCommits = commits.filter(
|
|
||||||
(commit) => (commit.data.inputs?.length ?? 0) > 0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const txDescription = this.deriveTransactionActivityDescription(
|
const txDescription = this.deriveTransactionActivityDescription(
|
||||||
invitation,
|
invitation,
|
||||||
context.template,
|
context.template,
|
||||||
@@ -355,7 +325,10 @@ export class HistoryService {
|
|||||||
context.variables,
|
context.variables,
|
||||||
);
|
);
|
||||||
const templateName = context.template?.name ?? "UnknownTemplate";
|
const templateName = context.template?.name ?? "UnknownTemplate";
|
||||||
const role = walletRole ?? "sender";
|
const role =
|
||||||
|
this.deriveCommitRoleIdentifier(commit, invitation, context.template) ??
|
||||||
|
walletRole ??
|
||||||
|
"sender";
|
||||||
const inputValue = this.resolveInputSatoshis(
|
const inputValue = this.resolveInputSatoshis(
|
||||||
txHash,
|
txHash,
|
||||||
inputIndex,
|
inputIndex,
|
||||||
@@ -422,13 +395,6 @@ export class HistoryService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: This is completely incorrect. It should be deriving the roles of the user based on the entity IDs in the commits, then mapping each commit to Inputs/Outputs so we know what belongs to the user.
|
|
||||||
* There are a few changes that will need to be made to make this work:
|
|
||||||
* 1. Provide a way to derive all entity IDs of the user for the invitation (If we are going with XPub)
|
|
||||||
* 2. Provide a way to get only the User's commits (and their inputs/outputs)
|
|
||||||
* 3. (Maybe) Include role on inputs and outputs - This one might be fine with just using the commit entity id
|
|
||||||
*/
|
|
||||||
private deriveWalletRolesForInvitation(
|
private deriveWalletRolesForInvitation(
|
||||||
context: InvitationContext,
|
context: InvitationContext,
|
||||||
outputs: HistoryUtxoItem[],
|
outputs: HistoryUtxoItem[],
|
||||||
@@ -444,33 +410,20 @@ export class HistoryService {
|
|||||||
roles.add("receiver");
|
roles.add("receiver");
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasInputCommit = (
|
for (const commit of context.walletCommits) {
|
||||||
context.walletEntityIdentifier
|
const role = this.deriveCommitRoleIdentifier(
|
||||||
? context.invitation.data.commits.filter(
|
commit,
|
||||||
(c) => c.entityIdentifier === context.walletEntityIdentifier,
|
|
||||||
)
|
|
||||||
: context.invitation.data.commits
|
|
||||||
).some((c) => (c.data.inputs?.length ?? 0) > 0);
|
|
||||||
|
|
||||||
if (hasInputCommit) roles.add("sender");
|
|
||||||
if (
|
|
||||||
!hasInputCommit &&
|
|
||||||
outputs.length === 0 &&
|
|
||||||
context.invitation.data.commits.some(
|
|
||||||
(c) => (c.data.inputs?.length ?? 0) > 0,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
roles.add("sender");
|
|
||||||
}
|
|
||||||
if (roles.size === 0) {
|
|
||||||
const inferred = this.extractInvitationRoleIdentifier(
|
|
||||||
context.invitation.data,
|
context.invitation.data,
|
||||||
context.template,
|
context.template,
|
||||||
context.walletEntityIdentifier,
|
|
||||||
);
|
);
|
||||||
if (inferred) roles.add(inferred);
|
if (role) roles.add(role);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasInputCommit = context.walletCommits.some(
|
||||||
|
(c) => (c.data.inputs?.length ?? 0) > 0,
|
||||||
|
);
|
||||||
|
if (hasInputCommit) roles.add("sender");
|
||||||
|
|
||||||
return roles.size > 0 ? Array.from(roles) : ["unknown"];
|
return roles.size > 0 ? Array.from(roles) : ["unknown"];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,7 +474,7 @@ export class HistoryService {
|
|||||||
const originKey = this.getUtxoOriginKey(
|
const originKey = this.getUtxoOriginKey(
|
||||||
utxo.templateIdentifier,
|
utxo.templateIdentifier,
|
||||||
utxo.outputIdentifier,
|
utxo.outputIdentifier,
|
||||||
utxo.lockingBytecode,
|
utxo.scriptHash,
|
||||||
);
|
);
|
||||||
return invitationByUtxoOrigin.get(originKey)?.invitationIdentifier;
|
return invitationByUtxoOrigin.get(originKey)?.invitationIdentifier;
|
||||||
}
|
}
|
||||||
@@ -533,59 +486,11 @@ export class HistoryService {
|
|||||||
const originKey = this.getUtxoOriginKey(
|
const originKey = this.getUtxoOriginKey(
|
||||||
utxo.templateIdentifier,
|
utxo.templateIdentifier,
|
||||||
utxo.outputIdentifier,
|
utxo.outputIdentifier,
|
||||||
utxo.lockingBytecode,
|
utxo.scriptHash,
|
||||||
);
|
);
|
||||||
return invitationByUtxoOrigin.get(originKey)?.roleIdentifier;
|
return invitationByUtxoOrigin.get(originKey)?.roleIdentifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
private resolveWalletEntityIdentifier(
|
|
||||||
invitation: Invitation,
|
|
||||||
ownUtxoOutpointKeys: Set<string>,
|
|
||||||
ownLockingBytecodes: Set<string>,
|
|
||||||
): string | undefined {
|
|
||||||
const scores = new Map<string, number>();
|
|
||||||
const addScore = (entityIdentifier: string, delta: number): void => {
|
|
||||||
scores.set(entityIdentifier, (scores.get(entityIdentifier) ?? 0) + delta);
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const commit of invitation.data.commits) {
|
|
||||||
for (const input of commit.data.inputs ?? []) {
|
|
||||||
const txHash = input.outpointTransactionHash
|
|
||||||
? input.outpointTransactionHash instanceof Uint8Array
|
|
||||||
? binToHex(input.outpointTransactionHash)
|
|
||||||
: String(input.outpointTransactionHash)
|
|
||||||
: undefined;
|
|
||||||
if (!txHash || input.outpointIndex === undefined) continue;
|
|
||||||
if (
|
|
||||||
ownUtxoOutpointKeys.has(
|
|
||||||
this.getOutpointKey(txHash, input.outpointIndex),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
addScore(commit.entityIdentifier, 3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const output of commit.data.outputs ?? []) {
|
|
||||||
const lockingBytecodeHex = output.lockingBytecode
|
|
||||||
? this.toLockingBytecodeHex(output.lockingBytecode)
|
|
||||||
: undefined;
|
|
||||||
if (!lockingBytecodeHex) continue;
|
|
||||||
if (ownLockingBytecodes.has(lockingBytecodeHex)) {
|
|
||||||
addScore(commit.entityIdentifier, 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let bestEntity: string | undefined;
|
|
||||||
let bestScore = 0;
|
|
||||||
for (const [entity, score] of scores.entries()) {
|
|
||||||
if (score > bestScore) {
|
|
||||||
bestScore = score;
|
|
||||||
bestEntity = entity;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return bestEntity;
|
|
||||||
}
|
|
||||||
|
|
||||||
private deriveUtxoDescription(
|
private deriveUtxoDescription(
|
||||||
utxo: UnspentOutputData,
|
utxo: UnspentOutputData,
|
||||||
template: XOTemplate | null,
|
template: XOTemplate | null,
|
||||||
@@ -715,27 +620,6 @@ export class HistoryService {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private extractInvitationRoleIdentifier(
|
|
||||||
invitation: XOInvitation,
|
|
||||||
template: XOTemplate | null,
|
|
||||||
walletEntityIdentifier?: string,
|
|
||||||
): string | undefined {
|
|
||||||
if (walletEntityIdentifier) {
|
|
||||||
const commits = invitation.commits.filter(
|
|
||||||
(commit) => commit.entityIdentifier === walletEntityIdentifier,
|
|
||||||
);
|
|
||||||
for (const commit of commits) {
|
|
||||||
const role = this.deriveCommitRoleIdentifier(
|
|
||||||
commit,
|
|
||||||
invitation,
|
|
||||||
template,
|
|
||||||
);
|
|
||||||
if (role) return role;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private inferRoleFromOutputIdentifier(
|
private inferRoleFromOutputIdentifier(
|
||||||
outputIdentifier: string,
|
outputIdentifier: string,
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type {
|
|||||||
AcceptInvitationParameters,
|
AcceptInvitationParameters,
|
||||||
AppendInvitationParameters,
|
AppendInvitationParameters,
|
||||||
Engine,
|
Engine,
|
||||||
FindSuitableResourcesParameters,
|
GetSpendableResourcesParameters,
|
||||||
} from "@xo-cash/engine";
|
} from "@xo-cash/engine";
|
||||||
import { hasInvitationExpired, mergeInvitationCommits } from "@xo-cash/engine";
|
import { hasInvitationExpired, mergeInvitationCommits } from "@xo-cash/engine";
|
||||||
import type {
|
import type {
|
||||||
@@ -85,8 +85,11 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
|||||||
throw new Error(`Template not found: ${invitation.templateIdentifier}`);
|
throw new Error(`Template not found: ${invitation.templateIdentifier}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// engine invitation (I have no idea if this is required)
|
||||||
|
const engineInvitation = await dependencies.engine.acceptInvitation(invitation);
|
||||||
|
|
||||||
// Create the invitation
|
// Create the invitation
|
||||||
const invitationInstance = new Invitation(invitation, dependencies);
|
const invitationInstance = new Invitation(engineInvitation, dependencies);
|
||||||
|
|
||||||
// Start the invitation and its tracking
|
// Start the invitation and its tracking
|
||||||
await invitationInstance.start();
|
await invitationInstance.start();
|
||||||
@@ -483,12 +486,27 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async findSuitableResources(
|
async findSuitableResources(
|
||||||
options: Partial<FindSuitableResourcesParameters> = {},
|
options: Partial<GetSpendableResourcesParameters> = {},
|
||||||
): Promise<UnspentOutputData[]> {
|
): Promise<UnspentOutputData[]> {
|
||||||
|
const templateIdentifier =
|
||||||
|
options.templateIdentifier ?? this.data.templateIdentifier;
|
||||||
|
const template = await this.engine.getTemplate(templateIdentifier);
|
||||||
|
const fallbackOutputIdentifier = Object.keys(template?.outputs ?? {})[0];
|
||||||
|
if (!fallbackOutputIdentifier && !options.outputIdentifier) {
|
||||||
|
throw new Error(
|
||||||
|
`No output identifiers found for template: ${templateIdentifier}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedOptions: GetSpendableResourcesParameters = {
|
||||||
|
templateIdentifier,
|
||||||
|
outputIdentifier: options.outputIdentifier ?? fallbackOutputIdentifier ?? "",
|
||||||
|
};
|
||||||
|
|
||||||
// Find the suitable resources
|
// Find the suitable resources
|
||||||
const { unspentOutputs } = await this.engine.findSuitableResources(
|
const { unspentOutputs } = await this.engine.getSpendableResources(
|
||||||
this.data,
|
this.data,
|
||||||
options,
|
resolvedOptions,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update the status of the invitation
|
// Update the status of the invitation
|
||||||
|
|||||||
@@ -100,8 +100,8 @@ export function TemplateListScreen(): React.ReactElement {
|
|||||||
for (const startingAction of rawStartingActions) {
|
for (const startingAction of rawStartingActions) {
|
||||||
const existing = actionMap.get(startingAction.action);
|
const existing = actionMap.get(startingAction.action);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
if (!existing.roles.includes(startingAction.role)) {
|
if (!existing.roles.includes(startingAction.role ?? '')) {
|
||||||
existing.roles.push(startingAction.role);
|
existing.roles.push(startingAction.role ?? '');
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -111,7 +111,7 @@ export function TemplateListScreen(): React.ReactElement {
|
|||||||
actionIdentifier: startingAction.action,
|
actionIdentifier: startingAction.action,
|
||||||
name: actionDef?.name || startingAction.action,
|
name: actionDef?.name || startingAction.action,
|
||||||
description: actionDef?.description,
|
description: actionDef?.description,
|
||||||
roles: [startingAction.role],
|
roles: [startingAction.role ?? ''],
|
||||||
source: 'starting',
|
source: 'starting',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -119,9 +119,9 @@ export function TemplateListScreen(): React.ReactElement {
|
|||||||
const ownedOutputIdentifiers = ownedOutputsByTemplate.get(templateIdentifier) ?? new Set<string>();
|
const ownedOutputIdentifiers = ownedOutputsByTemplate.get(templateIdentifier) ?? new Set<string>();
|
||||||
for (const outputIdentifier of ownedOutputIdentifiers) {
|
for (const outputIdentifier of ownedOutputIdentifiers) {
|
||||||
const outputDef = template.outputs?.[outputIdentifier];
|
const outputDef = template.outputs?.[outputIdentifier];
|
||||||
if (!outputDef || typeof outputDef.lockscript !== 'string') continue;
|
if (!outputDef || typeof outputDef.lockingScript !== 'string') continue;
|
||||||
|
|
||||||
const lockingScriptDefinition = (template.lockingScripts as Record<string, unknown> | undefined)?.[outputDef.lockscript] as
|
const lockingScriptDefinition = (template.lockingScripts as Record<string, unknown> | undefined)?.[outputDef.lockingScript] as
|
||||||
| { roles?: Record<string, { actions?: Array<{ action?: string; role?: string } | string> }> }
|
| { roles?: Record<string, { actions?: Array<{ action?: string; role?: string } | string> }> }
|
||||||
| undefined;
|
| undefined;
|
||||||
if (!lockingScriptDefinition?.roles) continue;
|
if (!lockingScriptDefinition?.roles) continue;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export { DataWizardFlow } from "./DataWizardFlow.js";
|
|||||||
*/
|
*/
|
||||||
export function createWizardFlow(action: XOTemplateAction): WizardFlow {
|
export function createWizardFlow(action: XOTemplateAction): WizardFlow {
|
||||||
if (action.data?.length && !action.transaction) {
|
if (action.data?.length && !action.transaction) {
|
||||||
return new DataWizardFlow(action.data);
|
return new DataWizardFlow([action.data]);
|
||||||
}
|
}
|
||||||
return new TransactionWizardFlow();
|
return new TransactionWizardFlow();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,13 +61,16 @@ export function RoleSelectStep({
|
|||||||
{availableRoles.length === 0 ? (
|
{availableRoles.length === 0 ? (
|
||||||
<Text color={colors.textMuted}>No roles available</Text>
|
<Text color={colors.textMuted}>No roles available</Text>
|
||||||
) : (
|
) : (
|
||||||
availableRoles.map((roleId, index) => {
|
availableRoles.map((roleId: string, index: number) => {
|
||||||
const isCursor =
|
const isCursor =
|
||||||
selectedRoleIndex === index && focusArea === 'content';
|
selectedRoleIndex === index && focusArea === 'content';
|
||||||
const roleDef = template.roles?.[roleId];
|
const roleDef = template.roles?.[roleId];
|
||||||
const actionRole = action?.roles?.[roleId];
|
const actionRole = action?.roles?.[roleId];
|
||||||
const requirements = actionRole?.requirements;
|
const requirements = actionRole?.requirements;
|
||||||
|
|
||||||
|
const actionRequirements = action?.requirements;
|
||||||
|
const actionRoleRequirements = actionRole && actionRequirements && actionRequirements.participants?.find((participant) => participant.role === roleId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box key={roleId} flexDirection="column" marginY={0}>
|
<Box key={roleId} flexDirection="column" marginY={0}>
|
||||||
<Text
|
<Text
|
||||||
@@ -96,10 +99,10 @@ export function RoleSelectStep({
|
|||||||
{' '}
|
{' '}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
{requirements.slots && requirements.slots.min > 0 && (
|
{actionRoleRequirements && actionRoleRequirements.slots && actionRoleRequirements.slots.min > 0 && (
|
||||||
<Text color={colors.textMuted} dimColor>
|
<Text color={colors.textMuted} dimColor>
|
||||||
{requirements.slots.min} input slot
|
{actionRoleRequirements.slots.min} input slot
|
||||||
{requirements.slots.min !== 1 ? 's' : ''}
|
{actionRoleRequirements.slots.min !== 1 ? 's' : ''}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ export function InvitationScreen(): React.ReactElement {
|
|||||||
// Two phases: first the ID input dialog, then the multi-step import flow.
|
// Two phases: first the ID input dialog, then the multi-step import flow.
|
||||||
const [showIdDialog, setShowIdDialog] = useState(false);
|
const [showIdDialog, setShowIdDialog] = useState(false);
|
||||||
const [importingId, setImportingId] = useState<string | null>(null);
|
const [importingId, setImportingId] = useState<string | null>(null);
|
||||||
|
const [pendingImportedInvitationId, setPendingImportedInvitationId] = useState<string | null>(null);
|
||||||
|
|
||||||
// ── Template cache ───────────────────────────────────────────────────────
|
// ── Template cache ───────────────────────────────────────────────────────
|
||||||
const [templateCache, setTemplateCache] = useState<Map<string, XOTemplate>>(new Map());
|
const [templateCache, setTemplateCache] = useState<Map<string, XOTemplate>>(new Map());
|
||||||
@@ -161,7 +162,7 @@ export function InvitationScreen(): React.ReactElement {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return [importItem, ...invitationItems];
|
return [importItem, ...invitationItems];
|
||||||
}, [invitations, templateCache]);
|
}, [invitations.length, templateCache]);
|
||||||
|
|
||||||
const selectedItem = listItems[selectedIndex];
|
const selectedItem = listItems[selectedIndex];
|
||||||
const selectedInvitation = selectedItem?.value ?? null;
|
const selectedInvitation = selectedItem?.value ?? null;
|
||||||
@@ -196,10 +197,30 @@ export function InvitationScreen(): React.ReactElement {
|
|||||||
/**
|
/**
|
||||||
* Import flow closed (completed or cancelled).
|
* Import flow closed (completed or cancelled).
|
||||||
*/
|
*/
|
||||||
const handleImportFlowClose = useCallback(() => {
|
const handleImportFlowClose = useCallback((importedInvitationId?: string) => {
|
||||||
|
if (importedInvitationId) {
|
||||||
|
setPendingImportedInvitationId(importedInvitationId);
|
||||||
|
}
|
||||||
setImportingId(null);
|
setImportingId(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Once imported invitation is visible in the list, select and focus it.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (!pendingImportedInvitationId) return;
|
||||||
|
|
||||||
|
const importedIndex = listItems.findIndex((item) => {
|
||||||
|
return item.value?.data.invitationIdentifier === pendingImportedInvitationId;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (importedIndex >= 0) {
|
||||||
|
setSelectedIndex(importedIndex);
|
||||||
|
setFocusedPanel('list');
|
||||||
|
setPendingImportedInvitationId(null);
|
||||||
|
}
|
||||||
|
}, [pendingImportedInvitationId, listItems]);
|
||||||
|
|
||||||
// ── Action handlers ────────────────────────────────────────────────────
|
// ── Action handlers ────────────────────────────────────────────────────
|
||||||
|
|
||||||
const acceptInvitation = useCallback(async () => {
|
const acceptInvitation = useCallback(async () => {
|
||||||
@@ -330,10 +351,10 @@ export function InvitationScreen(): React.ReactElement {
|
|||||||
const seenLockingBytecodes = new Set<string>();
|
const seenLockingBytecodes = new Set<string>();
|
||||||
|
|
||||||
for (const utxo of utxos) {
|
for (const utxo of utxos) {
|
||||||
const lockingBytecodeHex = utxo.lockingBytecode
|
const lockingBytecodeHex = utxo.scriptHash
|
||||||
? typeof utxo.lockingBytecode === 'string'
|
? typeof utxo.scriptHash === 'string'
|
||||||
? utxo.lockingBytecode
|
? utxo.scriptHash
|
||||||
: Buffer.from(utxo.lockingBytecode).toString('hex')
|
: Buffer.from(utxo.scriptHash).toString('hex')
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
if (lockingBytecodeHex && seenLockingBytecodes.has(lockingBytecodeHex)) continue;
|
if (lockingBytecodeHex && seenLockingBytecodes.has(lockingBytecodeHex)) continue;
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export function InvitationImportFlow({
|
|||||||
`Action: ${invitation?.data.actionIdentifier ?? 'Unknown'}`
|
`Action: ${invitation?.data.actionIdentifier ?? 'Unknown'}`
|
||||||
);
|
);
|
||||||
setStatus('Ready');
|
setStatus('Ready');
|
||||||
onClose();
|
onClose(invitation?.data.invitationIdentifier);
|
||||||
}, [selectedRole, template, invitation, showInfo, setStatus, onClose]);
|
}, [selectedRole, template, invitation, showInfo, setStatus, onClose]);
|
||||||
|
|
||||||
// ── Keyboard handling ────────────────────────────────────────────────────
|
// ── Keyboard handling ────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -116,8 +116,12 @@ export interface ImportFlowProps {
|
|||||||
mode: ImportFlowMode;
|
mode: ImportFlowMode;
|
||||||
/** The application service — injected, not pulled from context. */
|
/** The application service — injected, not pulled from context. */
|
||||||
appService: AppService;
|
appService: AppService;
|
||||||
/** Called when the flow completes or is cancelled. */
|
/**
|
||||||
onClose: () => void;
|
* Called when the flow completes or is cancelled.
|
||||||
|
* When import succeeds, the invitation identifier is provided so callers can
|
||||||
|
* select/focus the imported invitation in their UI.
|
||||||
|
*/
|
||||||
|
onClose: (importedInvitationId?: string) => void;
|
||||||
/** Display an error message to the user. */
|
/** Display an error message to the user. */
|
||||||
showError: (message: string) => void;
|
showError: (message: string) => void;
|
||||||
/** Display an info message to the user. */
|
/** Display an info message to the user. */
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ export const resolveActionRoles = (
|
|||||||
const starts = template.start ?? [];
|
const starts = template.start ?? [];
|
||||||
const roleIds = starts
|
const roleIds = starts
|
||||||
.filter((entry) => entry.action === actionIdentifier)
|
.filter((entry) => entry.action === actionIdentifier)
|
||||||
.map((entry) => entry.role);
|
.map((entry) => entry.role)
|
||||||
|
.filter((roleId) => roleId !== undefined);
|
||||||
|
|
||||||
return [...new Set(roleIds)];
|
return [...new Set(roleIds)];
|
||||||
};
|
};
|
||||||
@@ -60,17 +61,11 @@ export const roleRequiresInputs = (
|
|||||||
if (!action) return false;
|
if (!action) return false;
|
||||||
|
|
||||||
const actionRole = action.roles?.[roleIdentifier];
|
const actionRole = action.roles?.[roleIdentifier];
|
||||||
const roleSlotsMin = actionRole?.requirements?.slots?.min ?? 0;
|
const actionRequirements = action.requirements;
|
||||||
|
const actionRoleRequirements = actionRole && actionRequirements && actionRequirements.participants?.find((participant) => participant.role === roleIdentifier);
|
||||||
|
const roleSlotsMin = actionRoleRequirements && actionRoleRequirements.slots && actionRoleRequirements.slots.min > 0 ? actionRoleRequirements.slots.min : 0;
|
||||||
if (roleSlotsMin > 0) return true;
|
if (roleSlotsMin > 0) return true;
|
||||||
|
|
||||||
// Some templates specify slot/input requirements at action.requirements.roles
|
|
||||||
// instead of role.requirements. Respect those as well.
|
|
||||||
const roleRequirement = action.requirements?.roles?.find(
|
|
||||||
(requirement) => requirement.role === roleIdentifier,
|
|
||||||
);
|
|
||||||
const actionLevelSlotsMin = roleRequirement?.slots?.min ?? 0;
|
|
||||||
if (actionLevelSlotsMin > 0) return true;
|
|
||||||
|
|
||||||
const transactionIdentifier = action.transaction;
|
const transactionIdentifier = action.transaction;
|
||||||
const transaction = transactionIdentifier
|
const transaction = transactionIdentifier
|
||||||
? template.transactions?.[transactionIdentifier]
|
? template.transactions?.[transactionIdentifier]
|
||||||
@@ -132,13 +127,12 @@ export const resolveProvidedLockingBytecodeHex = (
|
|||||||
variableValues: Record<string, string>,
|
variableValues: Record<string, string>,
|
||||||
): string | undefined => {
|
): string | undefined => {
|
||||||
const outputDefinition = template.outputs?.[outputIdentifier];
|
const outputDefinition = template.outputs?.[outputIdentifier];
|
||||||
if (!outputDefinition || typeof outputDefinition.lockscript !== "string")
|
if (!outputDefinition || typeof outputDefinition.lockingScript !== "string") {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const lockingScriptDefinition = (
|
const lockingScriptDefinition = template.lockingScripts?.[outputDefinition.lockingScript];
|
||||||
template.lockingScripts as Record<string, unknown> | undefined
|
const scriptIdentifier = lockingScriptDefinition?.lockingBytecode;
|
||||||
)?.[outputDefinition.lockscript] as { lockingScript?: string } | undefined;
|
|
||||||
const scriptIdentifier = lockingScriptDefinition?.lockingScript;
|
|
||||||
if (!scriptIdentifier) return undefined;
|
if (!scriptIdentifier) return undefined;
|
||||||
|
|
||||||
const scriptExpression = (
|
const scriptExpression = (
|
||||||
|
|||||||
@@ -188,11 +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.
|
||||||
return {
|
return {
|
||||||
roleId: entry.role,
|
roleId: entry.role || '',
|
||||||
name: roleObj?.name || entry.role,
|
name: roleObj?.name || entry.role || '',
|
||||||
description: roleObj?.description,
|
description: roleObj?.description,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export const addFakeResource = async (
|
|||||||
outpointTransactionHash: options.outpointTransactionHash ?? randomTxHash(),
|
outpointTransactionHash: options.outpointTransactionHash ?? randomTxHash(),
|
||||||
minedAtHeight: options.minedAtHeight ?? 800000,
|
minedAtHeight: options.minedAtHeight ?? 800000,
|
||||||
valueSatoshis: options.valueSatoshis ?? 10000,
|
valueSatoshis: options.valueSatoshis ?? 10000,
|
||||||
lockingBytecode:
|
scriptHash:
|
||||||
options.lockingBytecode ??
|
options.lockingBytecode ??
|
||||||
"76a914000000000000000000000000000000000000000088ac",
|
"76a914000000000000000000000000000000000000000088ac",
|
||||||
reservedBy: options.reservedBy,
|
reservedBy: options.reservedBy,
|
||||||
@@ -131,7 +131,7 @@ export const unreserveResource = async (
|
|||||||
export const createMockEngine = async (seed: string) => {
|
export const createMockEngine = async (seed: string) => {
|
||||||
// Create the in-memory storage adapter.
|
// Create the in-memory storage adapter.
|
||||||
const storage = await createStorageAdapter({
|
const storage = await createStorageAdapter({
|
||||||
storageType: StorageType.INMEMORY,
|
storageType: "inmemory",
|
||||||
accountHash: binToHex(sha256.hash(convertMnemonicToSeedBytes(seed))),
|
accountHash: binToHex(sha256.hash(convertMnemonicToSeedBytes(seed))),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -165,6 +165,8 @@ export const createMockAppService = async (engine: Engine) => {
|
|||||||
const mockRates = new MockRatesService();
|
const mockRates = new MockRatesService();
|
||||||
const rates = new RatesService(mockRates);
|
const rates = new RatesService(mockRates);
|
||||||
|
|
||||||
|
const mockElectrum = new MockElectrumService();
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
syncServerUrl: "http://localhost:3000",
|
syncServerUrl: "http://localhost:3000",
|
||||||
engineConfig: {
|
engineConfig: {
|
||||||
@@ -174,5 +176,5 @@ export const createMockAppService = async (engine: Engine) => {
|
|||||||
invitationStoragePath: "test-invitations.db",
|
invitationStoragePath: "test-invitations.db",
|
||||||
};
|
};
|
||||||
|
|
||||||
return new AppService(engine, storage, config, rates);
|
return new AppService(engine, storage, config, mockElectrum, rates);
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user