Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ jobs:
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
with:
# The upgrade generator compiles the adapter from a pinned old commit.
fetch-depth: 0
- uses: dtolnay/rust-toolchain@stable

- name: Install Protoc
Expand All @@ -76,6 +79,11 @@ jobs:
repo-token: ${{ secrets.GITHUB_TOKEN }}

- uses: Swatinem/rust-cache@v2.7.8
with:
# Include the standalone old-version generator's dependency tree.
workspaces: |
. -> target
orange-sdk/tests/fixtures/cdk016-generator -> target

- name: Cache package-enabled Esplora
id: cache-esplora
Expand All @@ -94,3 +102,8 @@ jobs:

- name: Run cashu tests
run: cargo test --features _cashu-tests -- --test-threads=2 --nocapture

- name: Test funded Cashu upgrades from CDK 0.16
env:
TMPDIR: ${{ runner.temp }}
run: bash ci/test-cashu-upgrade.sh
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,38 @@ The wallet's behavior is controlled by `Tunables`:
- **Spark** - Custodial Lightning wallet (default)
- **Cashu** - Ecash-based wallet

### Upgrading Cashu wallets from CDK 0.16 to 0.18.3

Orange stores Cashu wallet records in its own KV namespaces within `orange.sqlite`
(or the configured remote store), rather than CDK's SQL wallet tables. On startup,
it automatically rekeys existing saga transactions to CDK's new transaction IDs
before wallet recovery runs. The migration copies before deleting, resumes after
interruption, and rejects conflicting records without overwriting them. Orange's
public payment IDs remain based on quote IDs.

Existing proofs, quotes, keysets, and in-flight sagas remain readable. CDK supplies
serde defaults for the new fields and accepts the old `payment_preimage` field.
Existing transactions default to completed; new transactions preserve their
pending/completed/failed status. A separate persistent derivation counter is
created as needed, leaving existing keyset counters and quote signing keys intact.
Legacy proof reservation indexes remain readable during recovery.

Startup runs saga recovery before starting payment tasks, on every open (including
wallets already restored from seed). Database initialization errors or an unreadable
recovery queue stop initialization. Individual failed recoveries are logged and
startup continues, so a mint outage does not block access to the rest of the wallet.
Unresolved operations retain their reservations and are retried on the next startup.

Back up the complete store before upgrading and open it with only one active
wallet instance. Downgrading an upgraded store is not supported; restore the
pre-upgrade backup if a rollback is needed. The test mint uses an ephemeral CDK
SQLite database; Orange does not manage persistent mint databases or `cdk-mintd`
configuration. Operators of a separate mint must follow the upstream
[CDK migration guide](https://github.com/cashubtc/cdk/blob/v0.18.2/docs/migrations/v0.18.md).

See the [CDK 0.18.2 release notes](https://github.com/cashubtc/cdk/releases/tag/v0.18.2)
and the upstream [transaction rekey migration](https://github.com/cashubtc/cdk/blob/v0.18.2/crates/cdk-sql-common/src/wallet/migrations/sqlite/20260726000000_rekey_saga_transactions.sql).

### Chain Sources
- Electrum servers
- Esplora servers (with optional Basic auth)
Expand Down
43 changes: 43 additions & 0 deletions ci/test-cashu-upgrade.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
set -euo pipefail

cd "$(dirname "$0")/.."
scratch=$(mktemp -d "${TMPDIR:-/tmp}/orange-cdk016.XXXXXXXX")
trap 'rm -rf "$scratch"' EXIT

# Always compile the old adapter, never the adapter under test. CI must fetch
# history containing this revision (actions/checkout with fetch-depth: 0).
old_revision=a3aa1c592625c45314004987430498307b9a1819
for source in cashu_store dyn_store; do
if [[ "$source" == cashu_store ]]; then
source_path=orange-sdk/src/trusted_wallet/cashu/cashu_store.rs
else
source_path=orange-sdk/src/dyn_store.rs
fi
git show "$old_revision:$source_path" > "$scratch/$source.rs"
done
# include! does not accept inner module documentation comments.
sed -i '/^\/\/!/d' "$scratch/dyn_store.rs"
export ORANGE_OLD_CASHU_STORE="$scratch/cashu_store.rs"
export ORANGE_OLD_DYN_STORE="$scratch/dyn_store.rs"

cargo build --manifest-path orange-sdk/tests/fixtures/cdk016-generator/Cargo.toml \
--message-format=json > "$scratch/build.json"
# Cargo reports the actual executable, honoring configured target directories.
ORANGE_CDK016_GENERATOR=$(python3 - "$scratch/build.json" <<'PY'
import json
import sys

with open(sys.argv[1]) as output:
binaries = [message['executable'] for line in output
if (message := json.loads(line)).get('reason') == 'compiler-artifact'
and message.get('executable')
and message['target']['name'] == 'orange-cdk016-fixture']
assert len(binaries) == 1, f'Expected one fixture generator, got {binaries}'
print(binaries[0])
PY
)
export ORANGE_CDK016_GENERATOR
test -x "$ORANGE_CDK016_GENERATOR"
cargo test -p orange-sdk --no-default-features --features _cashu-tests \
--lib cdk016_funded_wallet -- --ignored --nocapture --test-threads=2
15 changes: 9 additions & 6 deletions orange-sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ name = "orange_sdk"
default = ["spark"]
uniffi = ["dep:uniffi", "spark", "cashu", "rand", "pin-project-lite"]
spark = ["breez-sdk-spark", "uuid", "serde_json"]
cashu = ["cdk", "cdk/npubcash", "serde", "serde_json"]
cashu = ["cdk", "cdk/npubcash", "serde", "serde_json", "uuid"]
_test-utils = ["corepc-node", 'electrsd', "cashu", "uuid/v7", "rand"]
_cashu-tests = ["_test-utils", "cdk-ldk-node", "cdk/mint", "cdk-sqlite", "cdk-axum", "axum"]
_cashu-tests = ["_test-utils", "cdk-ldk-node", "cdk/mint", "cdk-sqlite", "cdk-axum", "cdk-fake-wallet", "axum"]

[dependencies]
graduated-rebalancer = { path = "../graduated-rebalancer", version = "0.1.0" }
Expand All @@ -31,21 +31,24 @@ rand = { version = "0.8.5", optional = true }
breez-sdk-spark = { git = "https://github.com/breez/spark-sdk.git", rev = "512453108958a7e31c870b30c8cc67d44fa3607b", default-features = false, optional = true }
tokio = { version = "1.0", default-features = false, features = ["rt-multi-thread", "sync", "macros"] }
uuid = { version = "1.0", default-features = false, optional = true }
cdk = { version = "0.16.0", default-features = false, features = ["wallet"], optional = true }
cdk = { version = "0.18.3", default-features = false, features = ["wallet"], optional = true }
serde = { version = "1.0", features = ["derive"], optional = true }
serde_json = { version = "1.0", optional = true }
async-trait = "0.1"
log = "0.4.28"

corepc-node = { version = "0.10.1", features = ["29_0", "download"], optional = true }
electrsd = { version = "0.36.1", default-features = false, features = ["legacy", "corepc-node_29_0"], optional = true }
cdk-ldk-node = { version = "0.16.0", optional = true }
cdk-sqlite = { version = "0.16.0", optional = true }
cdk-axum = { version = "0.16.0", optional = true }
cdk-ldk-node = { version = "0.18.3", optional = true }
cdk-sqlite = { version = "0.18.3", optional = true }
cdk-fake-wallet = { version = "0.18.3", optional = true }
cdk-axum = { version = "0.18.3", optional = true }
axum = { version = "0.8.1", optional = true }

uniffi = { version = "0.29", default-features = false, features = ["cli"], optional = true }
pin-project-lite = { version = "0.2", default-features = false, optional = true }

[dev-dependencies]
tempfile = "3"
cdk-common = { version = "=0.18.3", default-features = false, features = ["wallet", "test"] }
test-log = "0.2.18"
4 changes: 2 additions & 2 deletions orange-sdk/src/ffi/cashu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ impl From<CdkCurrencyUnit> for CurrencyUnit {
CdkCurrencyUnit::Usd => CurrencyUnit::Usd,
CdkCurrencyUnit::Eur => CurrencyUnit::Eur,
CdkCurrencyUnit::Auth => CurrencyUnit::Auth,
CdkCurrencyUnit::Custom(unit) => CurrencyUnit::Custom(unit),
CdkCurrencyUnit::Custom(unit) => CurrencyUnit::Custom(unit.to_string()),
_ => CurrencyUnit::Sat,
}
}
Expand All @@ -33,7 +33,7 @@ impl From<CurrencyUnit> for CdkCurrencyUnit {
CurrencyUnit::Usd => CdkCurrencyUnit::Usd,
CurrencyUnit::Eur => CdkCurrencyUnit::Eur,
CurrencyUnit::Auth => CdkCurrencyUnit::Auth,
CurrencyUnit::Custom(unit) => CdkCurrencyUnit::Custom(unit),
CurrencyUnit::Custom(unit) => CdkCurrencyUnit::Custom(unit.into()),
}
}
}
Expand Down
6 changes: 5 additions & 1 deletion orange-sdk/src/rebalancer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::lightning_wallet::LightningWallet;
use crate::logging::Logger;
use crate::store::{PaymentId, TxMetadata, TxMetadataStore, TxType};
use crate::trusted_wallet::DynTrustedWalletInterface;
use crate::{Event, EventQueue, PaymentType, Tunables};
use crate::{Event, EventQueue, PaymentType, Tunables, TxStatus};
use bitcoin_payment_instructions::amount::Amount;
use graduated_rebalancer::{RebalanceTrigger, RebalancerEvent, TriggerParams};
use ldk_node::lightning::util::logger::Logger as _;
Expand Down Expand Up @@ -74,6 +74,10 @@ impl RebalanceTrigger for OrangeTrigger {
// TODO: Maybe use this to backfill stuff we lost on crash?
continue;
}
// Only settled receipts can be recorded or selected as rebalance triggers.
if payment.status != TxStatus::Completed {
continue;
}
let payment_id = PaymentId::Trusted(payment.id);
let have_metadata = if let Some(metadata) =
self.tx_metadata.read().get(&payment_id)
Expand Down
Loading
Loading