Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
aff6a1e
Add safe Signet wallet templates and document local-only wallet files
MaiborodaY Mar 18, 2026
b074fc7
Add Signet offline receiver smoke test
MaiborodaY Mar 13, 2026
aba9d76
Resolve package.json after cherry-pick
MaiborodaY Mar 13, 2026
2b6e0e4
Add regtest/signet e2e tests and shared fixtures
MaiborodaY Mar 18, 2026
ede1779
Merge pull request #12 from MaiborodaY/feature/offline-receiver-e2e-t…
oleksandr-boostylabs Mar 19, 2026
7f6e49d
Change regtest E2E to manual trigger only
oleksandr-boostylabs Mar 19, 2026
cebaff7
Fix: remove npm cache requirement, use npm install instead of ci
oleksandr-boostylabs Mar 19, 2026
28985f8
Add workflows README and test results summary
oleksandr-boostylabs Mar 19, 2026
3f8c827
Fix summary table formatting
oleksandr-boostylabs Mar 19, 2026
045b9b7
Tighten regtest receiver assertions and expose restart inconsistency
MaiborodaY Mar 23, 2026
217d59f
Strengthen regtest receiver assertions and surface concurrency bugs
MaiborodaY Mar 23, 2026
07afd95
Add delayed-refresh regtest and tighten Signet smoke checks
MaiborodaY Mar 24, 2026
b9ad8d7
Strengthen Signet offline receiver coverage
MaiborodaY Mar 24, 2026
6c46201
Strengthen regtest parallel send coverage
MaiborodaY Mar 24, 2026
b2d2082
Update regtest receiver coverage and send flow tests
MaiborodaY Mar 25, 2026
030030b
Merge pull request #15 from MaiborodaY/feature/offline-receiver-e2e-t…
MaiborodaY Mar 25, 2026
85543b7
Introduce @utexo/rgb-sdk-core and migrate rgb-sdk (#14)
bandrivskiy Apr 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Workflows

## publish.yml — Publish to npm

Builds and publishes `@utexo/rgb-sdk` to npm.

- **Trigger**: manual
- **What it does**: builds the package with `tsup` and runs `npm publish --access public`

## regtest-e2e.yml — Regtest E2E Tests

Runs the full offline-receiver test suite (18 tests) against a local regtest environment.

- **Trigger**: manual (`workflow_dispatch`)
- **What it does**:
1. Clones `dcorral/test-rgb-proxy-playground` (Docker stack: bitcoind + electrs + rgb-proxy)
2. Starts the stack on `localhost` (proxy :3000, electrs :50001)
3. Runs `npm run test:regtest` — Jest tests from `tests/regtest/`
4. On failure, uploads `artifacts/` (JSON smoke reports) for debugging
5. Tears down the Docker stack
- **No secrets needed** — all credentials are hardcoded defaults from the playground
- **Test coverage**: blind/witness receive, relay-only mode, ACK guards, parallel sends, proxy restarts, expiry, donation paths
88 changes: 88 additions & 0 deletions .github/workflows/regtest-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: Regtest E2E Tests

on:
workflow_dispatch:

jobs:
regtest:
name: Regtest E2E
runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- name: Checkout rgb-sdk
uses: actions/checkout@v4

- name: Checkout test playground
uses: actions/checkout@v4
with:
repository: dcorral/test-rgb-proxy-playground
path: playground
submodules: recursive

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install dependencies
run: npm install

- name: Start regtest stack
working-directory: playground
run: |
./regtest.sh start
echo "Waiting for services to be ready..."
sleep 15

- name: Verify stack is healthy
run: |
curl -sf http://localhost:3000/json-rpc -X POST \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"server.info","params":{}}' \
&& echo "Proxy is up" || echo "Proxy not responding yet"
nc -z localhost 50001 && echo "Electrs is up" || echo "Electrs not responding yet"

- name: Run regtest E2E tests
env:
REGTEST_PROXY_HTTP_URL: http://localhost:3000/json-rpc
REGTEST_PROXY_RPC_URL: rpc://localhost:3000/json-rpc
REGTEST_BITCOIND_CONTAINER: bitcoind
REGTEST_BITCOIND_USER: user
REGTEST_BITCOIND_PASS: password
REGTEST_INDEXER_URL: tcp://localhost:50001
REGTEST_DATA_DIR: /tmp/rgb-e2e
REGTEST_PLAYGROUND_COMPOSE_FILE: ${{ github.workspace }}/playground/docker-compose.yaml
run: npm run test:regtest 2>&1 | tee /tmp/test-output.txt

- name: Generate summary
if: always()
run: |
echo "## Regtest E2E Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if grep -q "Tests:.*passed" /tmp/test-output.txt 2>/dev/null; then
SUITES=$(grep "Test Suites:" /tmp/test-output.txt | tail -1 | sed 's/Test Suites: //')
TESTS=$(grep "Tests:" /tmp/test-output.txt | tail -1 | sed 's/Tests: *//')
TIME=$(grep "Time:" /tmp/test-output.txt | tail -1 | sed 's/Time: *//')
echo "| | |" >> $GITHUB_STEP_SUMMARY
echo "|---|---|" >> $GITHUB_STEP_SUMMARY
echo "| **Test Suites** | $SUITES |" >> $GITHUB_STEP_SUMMARY
echo "| **Tests** | $TESTS |" >> $GITHUB_STEP_SUMMARY
echo "| **Time** | $TIME |" >> $GITHUB_STEP_SUMMARY
else
echo "> Could not parse test results. Check logs." >> $GITHUB_STEP_SUMMARY
fi

- name: Upload smoke reports on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: regtest-smoke-reports
path: artifacts/
retention-days: 7
if-no-files-found: ignore

- name: Stop regtest stack
if: always()
working-directory: playground
run: ./regtest.sh stop
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ dist/
scripts/
package.docs.json
cli/data/*
.rgb-wallet/
!cli/data/*.example.json
.rgb-wallet/
artifacts/
17 changes: 5 additions & 12 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,7 @@

This is the underlying SDK used by RGB client applications. It provides a complete set of TypeScript/Node.js bindings for managing RGB-based transfers using **rgb-protocol libraries**

⚠️ **Security Notice**
If you're migrating from the legacy `rgb-sdk` (which relied on a remote RGB Node server), be aware that wallet metadata such as xpubs may have been exposed and this cannot be undone.

If you're upgrading from `rgb-sdk` to `@utexo/rgb-sdk`, see the **[Migration Guide](./MIGRATION.md)** for step-by-step instructions on moving your wallet state to local storage.

For full details on security implications and recommended actions, please read **[SECURITY.md](./SECURITY.md)**.

> **RGB Protocol**: This SDK uses the [`rgb-lib`](https://github.com/RGB-Tools/rgb-lib) binding library to interact with the RGB protocol. All operations are performed locally, providing full control over wallet data and operations.
**RGB Protocol**: This SDK uses the [`rgb-lib`](https://github.com/RGB-Tools/rgb-lib) binding library to interact with the RGB protocol. All operations are performed locally, providing full control over wallet data and operations.

---

Expand Down Expand Up @@ -126,18 +119,20 @@ The SDK uses default endpoints for RGB transport and Bitcoin indexing. These are

**Transport Endpoints** (RGB protocol communication):

- **UTEXO**: `rpcs://rgb-proxy-utexo.utexo.com/json-rpc`
- **Mainnet**: `rpcs://rgb-proxy-mainnet.utexo.com/json-rpc`
- **Testnet**: `rpcs://rgb-proxy-testnet3.utexo.com/json-rpc`
- **Testnet4**: `rpcs://proxy.iriswallet.com/0.2/json-rpc`
- **Signet**: `rpcs://rgb-proxy-utexo.utexo.com/json-rpc`
- **Signet**: `rpcs://proxy.iriswallet.com/0.2/json-rpc`
- **Regtest**: `rpcs://proxy.iriswallet.com/0.2/json-rpc`

**Indexer URLs** (Bitcoin blockchain data):

- **UTEXO**: `https://esplora-api.utexo.com`
- **Mainnet**: `ssl://electrum.iriswallet.com:50003`
- **Testnet**: `ssl://electrum.iriswallet.com:50013`
- **Testnet4**: `ssl://electrum.iriswallet.com:50053`
- **Signet**: `https://esplora-api.utexo.com`
- **Signet**: `ssl://electrum.iriswallet.com:50033`
- **Regtest**: `tcp://regtest.thunderstack.org:50001`

UTEXOWallet uses network (`testnet` / `mainnet`) that define indexer and transport endpoints internally.
Expand Down Expand Up @@ -174,8 +169,6 @@ console.log('BTC balance:', balance);
await wallet.dispose();
```

---

## Core Workflows

### Wallet Initialization
Expand Down
4 changes: 2 additions & 2 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ utexo generate_keys mywallet testnet
**Parameters:**
- `wallet_name` (required) - Name for the wallet configuration file
- `network` (optional) - Bitcoin network, defaults to `regtest` if not provided
- Options: `mainnet`, `testnet`, `testnet4`, `regtest`, `signet`
- Options: `mainnet`, `testnet`, `testnet4`, `regtest`, `utexo`

**Output:**
- Creates a JSON file in `cli/data/<wallet_name>.json` containing:
Expand Down Expand Up @@ -345,4 +345,4 @@ Scripts read and write wallet configs under `data/` automatically.
- All scripts use ES modules (`.mjs` extension)
- Scripts require the project to be built (`npm run build`) before use
- Wallet files are stored in `cli/data/` as `<wallet_name>.json`
- Wallet `network` from config (e.g. `regtest`, `signet`, `testnet`) is mapped to UTEXOWallet presets `mainnet` or `testnet` automatically
- Wallet `network` from config (e.g. `regtest`, `utexo`, `testnet`) is mapped to UTEXOWallet presets `mainnet` or `testnet` automatically
11 changes: 11 additions & 0 deletions cli/data/stage2-receiver.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"walletName": "stage2-receiver",
"network": "signet",
"mnemonic": "REPLACE_WITH_12_OR_24_WORD_MNEMONIC",
"xpub": "REPLACE_WITH_XPUB",
"accountXpubVanilla": "REPLACE_WITH_ACCOUNT_XPUB_VANILLA",
"accountXpubColored": "REPLACE_WITH_ACCOUNT_XPUB_COLORED",
"masterFingerprint": "REPLACE_WITH_FINGERPRINT",
"xpriv": "REPLACE_WITH_XPRIV",
"createdAt": "REPLACE_WITH_ISO_TIMESTAMP"
}
11 changes: 11 additions & 0 deletions cli/data/stage2-sender.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"walletName": "stage2-sender",
"network": "signet",
"mnemonic": "REPLACE_WITH_12_OR_24_WORD_MNEMONIC",
"xpub": "REPLACE_WITH_XPUB",
"accountXpubVanilla": "REPLACE_WITH_ACCOUNT_XPUB_VANILLA",
"accountXpubColored": "REPLACE_WITH_ACCOUNT_XPUB_COLORED",
"masterFingerprint": "REPLACE_WITH_FINGERPRINT",
"xpriv": "REPLACE_WITH_XPRIV",
"createdAt": "REPLACE_WITH_ISO_TIMESTAMP"
}
2 changes: 1 addition & 1 deletion examples/create-utxos-asset.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import { UTEXOWallet } from '../dist/index.mjs';

const NETWORK = 'testnet';
const MNEMONIC = "drastic vacuum age family between general melody elbow ball very require pulp";
const MNEMONIC = "tobacco dinner advice together repeat digital need cancel lift near blind cute";
// drastic vacuum age family between general melody elbow ball very require pulp
// const MNEMONIC = process.env.MNEMONIC || 'apple deposit job second wear metal zebra target filter chunk pill dynamic';
// const MNEMONIC = process.env.MNEMONIC || 'famous hurt miss favorite pitch rich rude cricket fault hammer split guilt';
Expand Down
19 changes: 11 additions & 8 deletions examples/transfer.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
import { UTEXOWallet } from '../dist/index.mjs';

const NETWORK = 'testnet';
const MNEMONIC_A = process.env.MNEMONIC_A || 'top reject between sugar rug pulse radar coffee kiss faculty pool vocal';
const MNEMONIC_B = process.env.MNEMONIC_B || 'famous hurt miss favorite pitch rich rude cricket fault hammer split guilt';
const ASSET_ID = process.env.ASSET_ID||'rgb:GE5hMbGS-TdK60Sf-V4TNAUM-zb0228l-4yi_qEh-PiMHLOg';
const MNEMONIC_A = process.env.MNEMONIC_A || 'paddle smooth humble inherit reason basic brave clerk absorb later text that';
const MNEMONIC_B = process.env.MNEMONIC_B || 'tobacco dinner advice together repeat digital need cancel lift near blind cute';
const ASSET_ID = process.env.ASSET_ID||'rgb:4PhQDg98-kFPjSKO-HdbJOXo-IWt6P~a-HeVZA8L-A~tvBNU';
if (!ASSET_ID) {
console.error('ASSET_ID is required (e.g. from create-utxos-asset.mjs output)');
process.exit(1);
Expand All @@ -31,6 +31,10 @@ async function main() {
try {
await walletA.initialize();
await walletB.initialize();

await walletB.refreshWallet();
await walletA.refreshWallet();

console.log('Wallet A address:', await walletA.getAddress());
console.log('Wallet B address:', await walletB.getAddress());

Expand Down Expand Up @@ -63,18 +67,17 @@ async function main() {
await walletB.refreshWallet();
await walletA.refreshWallet();

const transfersA = await walletA.listTransfers(ASSET_ID); // sent stansfer should be settled
const transfersB = await walletB.listTransfers(ASSET_ID); // received transfer should be settled
console.log('Wallet A listTransfers:', transfersA.length, transfersA);
console.log('Wallet B listTransfers:', transfersB.length, transfersB);
// const transfersA = await walletA.listTransfers(ASSET_ID); // sent stansfer should be settled
// const transfersB = await walletB.listTransfers(ASSET_ID); // received transfer should be settled
// console.log('Wallet A listTransfers:', transfersA.length, transfersA);
// console.log('allet B listTransfers:', transfersB.length, transfersB);
} finally {
await walletA.dispose();
await walletB.dispose();
}

console.log('Done.');
}

main()
.then(() => process.exit(0))
.catch((err) => {
Expand Down
22 changes: 11 additions & 11 deletions examples/utexo-vss-backup-restore.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import path from 'path';
import { UTEXOWallet, restoreUtxoWalletFromVss } from '../dist/index.mjs';

const MNEMONIC = 'top reject between sugar rug pulse radar coffee kiss faculty pool vocal';
const MNEMONIC = 'paddle smooth humble inherit reason basic brave clerk absorb later text that';
const TARGET_DIR = path.join(process.cwd(), 'restored-utexo-vss');

async function runVssBackup() {
Expand Down Expand Up @@ -41,16 +41,16 @@ async function runVssRestore() {
});
console.log('Restored directory:', restoredDir);

// const wallet = new UTEXOWallet(MNEMONIC, { dataDir: restoredDir, network: 'testnet' });
// try {
// await wallet.initialize();
// const address = await wallet.getAddress();
// console.log('Restored wallet address:', address);
// const balance = await wallet.getBtcBalance();
// console.log('BTC balance:', balance);
// } finally {
// await wallet.dispose();
// }
const wallet = new UTEXOWallet(MNEMONIC, { dataDir: restoredDir, network: 'testnet' });
try {
await wallet.initialize();
const address = await wallet.getAddress();
console.log('Restored wallet address:', address);
const balance = await wallet.getBtcBalance();
console.log('BTC balance:', balance);
} finally {
await wallet.dispose();
}
}

const runRestore = true; // true = run VSS restore instead of backup
Expand Down
4 changes: 2 additions & 2 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,17 @@ module.exports = {
diagnostics: {
ignoreCodes: [1343, 2351, 6059, 7016], // Ignore missing declaration file errors
},
isolatedModules: true,
useESM: true,
},
],
},
// Don't transform ESM modules - let Node.js handle them with experimental flags
transformIgnorePatterns: [
'node_modules/(?!(@metamask|bitcoindevkit|@types)/)',
'node_modules/(?!(@metamask|bitcoindevkit|@types|@noble)/)',
],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'^@noble/hashes/sha2$': '<rootDir>/node_modules/@noble/hashes/sha2.js',
},
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts', '!src/**/index.ts'],
testTimeout: 30000, // Increased timeout for crypto operations
Expand Down
35 changes: 35 additions & 0 deletions jest.regtest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/** @type {import('jest').Config} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/tests'],
testMatch: ['**/regtest/**/*.test.ts'],
extensionsToTreatAsEsm: ['.ts'],
transform: {
'^.+\\.ts$': [
'ts-jest',
{
tsconfig: {
target: 'ES2020',
module: 'ESNext',
moduleResolution: 'node',
esModuleInterop: true,
allowSyntheticDefaultImports: true,
skipLibCheck: true,
},
diagnostics: {
ignoreCodes: [1343, 2351, 6059, 7016],
},
useESM: true,
},
],
},
transformIgnorePatterns: [
'node_modules/(?!(@metamask|bitcoindevkit|@types)/)',
],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
testTimeout: 120000,
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
};
35 changes: 35 additions & 0 deletions jest.signet.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/** @type {import('jest').Config} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/tests'],
testMatch: ['**/signet/**/*.test.ts'],
extensionsToTreatAsEsm: ['.ts'],
transform: {
'^.+\\.ts$': [
'ts-jest',
{
tsconfig: {
target: 'ES2020',
module: 'ESNext',
moduleResolution: 'node',
esModuleInterop: true,
allowSyntheticDefaultImports: true,
skipLibCheck: true,
},
diagnostics: {
ignoreCodes: [1343, 2351, 6059, 7016],
},
useESM: true,
},
],
},
transformIgnorePatterns: [
'node_modules/(?!(@metamask|bitcoindevkit|@types)/)',
],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
testTimeout: 600000,
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
};
Loading
Loading