Merge branch 'moria-v11-test18' into 'master'

moria v1-1: TEST18 categories and riftenlabs-defi 0.5.1

See merge request riftenlabs/riftenlabs-indexer!103
This commit is contained in:
dagurval-agent 2026-08-21 11:37:09 +00:00
commit fb6f417759
6 changed files with 254 additions and 41 deletions

4
Cargo.lock generated
View file

@ -2283,9 +2283,9 @@ dependencies = [
[[package]]
name = "riftenlabs-defi"
version = "0.4.6"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcf89dd770556a74ecd88b6971e7fa151f58bcee5b1a400b836c485c668973b"
checksum = "e7541d5d2101a03925b9de89846c1e9a66bcdcdf6a4435a7bcf30bf294bb236f"
dependencies = [
"anyhow",
"bitcoin_hashes",

View file

@ -22,7 +22,7 @@ serde_json = "1.0.133"
rocket = { version = "0.5.1", features = ["json"] }
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] }
rayon = "1.10.0"
riftenlabs-defi = "0.4.6"
riftenlabs-defi = "0.5.1"
rocket_cors = "0.6.0"
log = "0.4"
stderrlog = "0.6.0"

View file

@ -3,40 +3,41 @@
// This software is licensed under the GNU Affero General Public License (AGPL), version 3.0 or later.
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html
//! Per-network Moria v1-1 deployment constants.
//! Per-network Moria v1-1 TEST18 deployment constants.
//!
//! Mainnet is unconfigured until a v1-1 deployment exists (fail-closed).
//! Chipnet is TEST18. Mainnet is unconfigured until a v1-1 deployment exists
//! (fail-closed).
use bitcoincash::{Network, TokenID};
use riftenlabs_defi::moria_v1_1::MoriaV11TokenIds;
/// Unique tail of the moria v1-1 redeem script. Partial scriptsig match —
/// every moria method spends this body.
/// Unique tail of the moria v1-1 TEST18 redeem script. Partial scriptsig match —
/// every moria method spends this body. Last 20 bytes of REDEEMSCRIPT_PROD.
const REDEEM_TAIL: [u8; 20] = [
0x51, 0xd2, 0x51, 0xcf, 0x88, 0x52, 0xd1, 0x00, 0x88, 0xc4, 0x53, 0x9c, 0x77, 0x77, 0x77, 0x68,
0xd2, 0x51, 0xcf, 0x88, 0x52, 0xd1, 0x00, 0x88, 0xc4, 0x53, 0x9d, 0x6d, 0x6d, 0x51, 0x68, 0x68,
0x68, 0x68, 0x68, 0x68,
];
/// Chipnet v1-1 token categories and loan P2SH32, or `None` on other networks.
/// Chipnet TEST18 token categories and loan P2SH32, or `None` on other networks.
pub fn token_ids(network: Option<Network>) -> Option<MoriaV11TokenIds> {
match network {
Some(Network::Chipnet) => Some(MoriaV11TokenIds {
moria: "56d909e16cd3b4cecae685a7ca45baa292bf7550205c2ad59320a342c73754de"
moria: "1c6dc7c7ad3e7e37fa1c84e715cd22a28177f52a2c49853e6e0b638505c29637"
.parse::<TokenID>()
.expect("valid chipnet moria v1-1 token_id"),
bp_oracle: "a0628bc21d10211a23f986fd12770e836bae43878db93db8b164ab8a5ab33c96"
.expect("valid chipnet TEST18 moria token_id"),
bp_oracle: "640d2c75cb1d5d15dbaf26022b37661d3578485a6788736893c1331ed2384905"
.parse::<TokenID>()
.expect("valid chipnet bp_oracle v1-1 token_id"),
pool: "a533b7e96435af18136b534b43e465dd759db3ac33e8490b9bfd254b8c27ae48"
.expect("valid chipnet TEST18 bp_oracle token_id"),
pool: "8aaa8012b206f45b33472a55b5c3c328bae0eb33e4ee466f7f9cbade4750225c"
.parse::<TokenID>()
.expect("valid chipnet pool v1-1 token_id"),
bar: "bc98b2733ca1c2070586cce1a4216418830ccd9b19ff77b5d43d47542bb3b50d"
.expect("valid chipnet TEST18 pool token_id"),
bar: "779df3e91141f0ea414c477f61d08a3c55b69cd91eb58e42953d8c7eaec84bb9"
.parse::<TokenID>()
.expect("valid chipnet bar v1-1 token_id"),
.expect("valid chipnet TEST18 bar token_id"),
loan_locking_bytecode: hex::decode(
"aa20415812e068086c90686635a813d77b6c412427e574e89b16e79feeaa48cf0f0787",
"aa208bbc3b3f65b354e69a93175c70f3cbb619154ae2902878521c17edcbd83cc12f87",
)
.expect("valid chipnet loan locking bytecode"),
.expect("valid chipnet TEST18 loan locking bytecode"),
}),
_ => None,
}

View file

@ -544,7 +544,7 @@ pub async fn index_moria_v11(
action.loan_commitment = Some(LoanCommitmentV11 {
borrower_nft_hash: borrower,
delegate_nft_hash: delegate,
principal: principal as u16,
principal: principal as u32,
annual_interest_rate_bp: interest_rate as u16,
timestamp: loan_ts as u64,
});
@ -565,7 +565,10 @@ pub async fn index_moria_v11(
}
}
// Create / replace continuing loan UTXO.
// Create / replace continuing loan UTXO. Refinance, borrow, retarget,
// add-collateral, and partial redeem/repay (continuation NFT via
// `new_loan_commitment` + `loan_output_index`) all land here. Full
// close has no continuation and the spent UTXO stays deleted.
if let Some(output_idx) = action.loan_output_index {
if let Some(commitment) = action
.new_loan_commitment
@ -581,7 +584,6 @@ pub async fn index_moria_v11(
blockhash,
)
.await?;
// Policy lives on the allowance, revealed at first spend.
}
}

View file

@ -46,7 +46,8 @@ pub struct MoriaV11Entry {
pub action_type: &'static str,
pub borrower_hash: Option<String>,
pub delegate_hash: Option<String>,
/// Encoded principal in whole MUSD (× [`MUSD_BASE_UNITS_PER_MUSD`](riftenlabs_defi::moria_v1_1::MUSD_BASE_UNITS_PER_MUSD) for base units).
/// Whole-MUSD principal (`i64`, not `u16`; 128_000 fits). Multiply by
/// [`MUSD_BASE_UNITS_PER_MUSD`](riftenlabs_defi::moria_v1_1::MUSD_BASE_UNITS_PER_MUSD) for base units.
pub principal: Option<i64>,
pub interest_rate: Option<i64>,
pub loan_timestamp: Option<i64>,
@ -71,7 +72,8 @@ pub struct MoriaV11LoanUtxo {
pub vout: i64,
pub borrower_hash: String,
pub delegate_hash: String,
/// Encoded principal in whole MUSD (× [`MUSD_BASE_UNITS_PER_MUSD`](riftenlabs_defi::moria_v1_1::MUSD_BASE_UNITS_PER_MUSD) for base units).
/// Whole-MUSD principal (`i64`, not `u16`; 128_000 fits). Multiply by
/// [`MUSD_BASE_UNITS_PER_MUSD`](riftenlabs_defi::moria_v1_1::MUSD_BASE_UNITS_PER_MUSD) for base units.
pub principal: i64,
pub interest_rate: i64,
pub loan_timestamp: i64,
@ -117,7 +119,8 @@ pub struct MoriaV11Crankable {
pub vout: i64,
pub borrower_hash: String,
pub delegate_hash: String,
/// Encoded principal in whole MUSD (× [`MUSD_BASE_UNITS_PER_MUSD`](riftenlabs_defi::moria_v1_1::MUSD_BASE_UNITS_PER_MUSD) for base units).
/// Whole-MUSD principal (`i64`, not `u16`; 128_000 fits). Multiply by
/// [`MUSD_BASE_UNITS_PER_MUSD`](riftenlabs_defi::moria_v1_1::MUSD_BASE_UNITS_PER_MUSD) for base units.
pub principal: i64,
pub interest_rate: i64,
pub loan_timestamp: i64,

View file

@ -15,16 +15,17 @@ use riftenlabs_defi::moria_v1_1::{
crank_allowance_locking_bytecode, crank_hint_to_policy, decode_crank_hint,
encode_loan_commitment_v11, principal_to_base_units, CrankPolicy, LoanCommitmentV11,
MoriaV11TokenIds, ALLOWANCE_METHOD_CLOSE, ALLOWANCE_METHOD_CRANK, DEFAULT_DOWN_LEG_SECONDS,
LOAN_METHOD_RETARGET,
LOAN_METHOD_REDEEM, LOAN_METHOD_REPAY, LOAN_METHOD_RETARGET,
};
use sqlx::SqlitePool;
/// Whole-MUSD → base units as i64 (DB/API). Scale lives only in defi `MUSD_BASE_UNITS_PER_MUSD`.
fn principal_base_units(principal_musd: u16) -> i64 {
fn principal_base_units(principal_musd: u32) -> i64 {
principal_to_base_units(principal_musd) as i64
}
const MORIA_METHOD_BORROW: u8 = 2;
const MORIA_METHOD_REPAY: u8 = 0;
const MORIA_METHOD_RETARGET: u8 = 4;
async fn setup(pool: SqlitePool) {
@ -46,7 +47,7 @@ fn token_ids() -> MoriaV11TokenIds {
}
fn test_loan_lock() -> Vec<u8> {
hex::decode("aa20415812e068086c90686635a813d77b6c412427e574e89b16e79feeaa48cf0f0787").unwrap()
hex::decode("aa208bbc3b3f65b354e69a93175c70f3cbb619154ae2902878521c17edcbd83cc12f87").unwrap()
}
fn token_ids_with_loan_lock() -> MoriaV11TokenIds {
@ -194,7 +195,7 @@ fn make_tx(inputs: Vec<TxIn>, outputs: Vec<TxOut>) -> Transaction {
}
}
fn commitment(borrower: [u8; 32], delegate: [u8; 32], principal: u16, bp: u16, ts: u64) -> Vec<u8> {
fn commitment(borrower: [u8; 32], delegate: [u8; 32], principal: u32, bp: u16, ts: u64) -> Vec<u8> {
encode_loan_commitment_v11(&LoanCommitmentV11 {
borrower_nft_hash: borrower,
delegate_nft_hash: delegate,
@ -508,10 +509,8 @@ async fn index_crank_updates_allowance_in_place() {
.unwrap();
let old = commitment(borrower(), delegate(), 1_000, 500, 1_700_000_000);
let mut new = old.clone();
new[66] = 0x20;
new[67] = 0x03;
new[68] = 0x20;
// 75-byte: rate @67, timestamp @69 (frozen head [0,67) includes principal).
let new = commitment(borrower(), delegate(), 1_000, 0x0320, 1_700_000_016);
let interest = 15i64;
let allowance_remaining = 500i64;
@ -667,18 +666,13 @@ async fn index_two_loan_retarget_batch() {
let bh = blockhash(0xCC);
let old0 = commitment(borrower(), delegate(), 1_000, 500, 1_700_000_000);
let mut new0 = old0.clone();
new0[66] = 0xF4;
new0[67] = 0x01;
new0[68] = 0x10;
// 75-byte layout: rate @67 and timestamp @69 may change; frozen head [0,67).
let new0 = commitment(borrower(), delegate(), 1_000, 500, 1_700_000_016);
let borrower2 = [0x33; 32];
let delegate2 = [0x44; 32];
let old1 = commitment(borrower2, delegate2, 2_000, 600, 1_700_000_000);
let mut new1 = old1.clone();
new1[66] = 0x58;
new1[67] = 0x02;
new1[68] = 0x10;
let new1 = commitment(borrower2, delegate2, 2_000, 600, 1_700_000_016);
let seed_txid0 = Txid::from_byte_array([0xA0; 32]);
let seed_txid1 = Txid::from_byte_array([0xA1; 32]);
@ -1086,3 +1080,216 @@ async fn mempool_eviction_drops_unconfirmed_loan() {
assert!(get_active_loans(pool).await.unwrap().is_empty());
assert!(get_unconfirmed_txids(pool).await.unwrap().is_empty());
}
fn partial_repay_tx(
ids: &MoriaV11TokenIds,
remaining: Vec<u8>,
remaining_sats: u64,
) -> Transaction {
make_tx(
vec![
txin(0x01, 0, unlock(MORIA_METHOD_REPAY, &[])),
txin(0x02, 0, unlock(0, &[])),
txin(0xA0, 2, unlock(LOAN_METHOD_REPAY, &[])),
txin(0xB0, 0, ScriptBuf::new()),
],
vec![
out(1_000, Some(minting(ids.moria, vec![0x02], 1_100_000))),
out(1_000, Some(nft(tid(0xD1), vec![0x11; 16]))),
out(1_000, Some(ft(ids.moria, 42))),
bare(1_000),
out(remaining_sats, Some(nft(ids.moria, remaining))),
bare(1_000),
],
)
}
fn partial_redeem_tx(
ids: &MoriaV11TokenIds,
remaining: Vec<u8>,
remaining_sats: u64,
) -> Transaction {
make_tx(
vec![
txin(0x01, 0, unlock(MORIA_METHOD_REPAY, &[])),
txin(0x02, 0, unlock(0, &[])),
txin(0xA0, 2, unlock(LOAN_METHOD_REDEEM, &[])),
txin(0xB2, 0, ScriptBuf::new()),
],
vec![
out(1_000, Some(minting(ids.moria, vec![0x02], 1_050_000))),
out(1_000, Some(nft(tid(0xD1), vec![0x11; 16]))),
out(1_000, Some(ft(ids.moria, 77))),
out(1_000, Some(nft(ids.bp_oracle, vec![0x22; 26]))),
out(remaining_sats, Some(nft(ids.moria, remaining))),
bare(1_000),
],
)
}
#[rocket::async_test]
async fn partial_repay_keeps_continuation_loan_utxo() {
let db = crate::utiltest::mock_db_pool(setup).await;
let pool = &db.moria_w;
let ids = token_ids();
let bh = blockhash(0xE1);
let seed = Txid::from_byte_array([0xA0; 32]);
insert_loan_utxo(
pool,
&seed,
2,
&LoanCommitmentV11 {
borrower_nft_hash: borrower(),
delegate_nft_hash: delegate(),
principal: 20_000,
annual_interest_rate_bp: 500,
timestamp: 1_700_000_000,
},
9_000_000,
&bh,
)
.await
.unwrap();
let remaining = commitment(borrower(), delegate(), 15_000, 500, 1_700_000_000);
let tx = partial_repay_tx(&ids, remaining, 6_000_000);
let n = index_moria_v11(pool, &[tx.clone()], &bh, 1_700_100_000, &ids)
.await
.unwrap();
assert_eq!(n, 1);
let history = get_loan_history(pool, &borrower()).await.unwrap();
assert_eq!(history.len(), 1);
assert_eq!(history[0].action_type, "repay");
assert_eq!(history[0].new_principal, Some(15_000));
let active = get_active_loans(pool).await.unwrap();
assert_eq!(
active.len(),
1,
"partial repay must keep the continuation UTXO"
);
assert_eq!(active[0].principal, 15_000);
assert_eq!(active[0].txid, tx.compute_txid().to_string());
}
#[rocket::async_test]
async fn partial_redeem_keeps_continuation_loan_utxo() {
let db = crate::utiltest::mock_db_pool(setup).await;
let pool = &db.moria_w;
let ids = token_ids();
let bh = blockhash(0xE2);
let seed = Txid::from_byte_array([0xA0; 32]);
insert_loan_utxo(
pool,
&seed,
2,
&LoanCommitmentV11 {
borrower_nft_hash: borrower(),
delegate_nft_hash: delegate(),
principal: 20_000,
annual_interest_rate_bp: 500,
timestamp: 1_700_000_000,
},
9_000_000,
&bh,
)
.await
.unwrap();
let remaining = commitment(borrower(), delegate(), 10_000, 500, 1_700_000_000);
let tx = partial_redeem_tx(&ids, remaining, 4_000_000);
index_moria_v11(pool, &[tx.clone()], &bh, 1_700_100_000, &ids)
.await
.unwrap();
let history = get_loan_history(pool, &borrower()).await.unwrap();
assert_eq!(history[0].action_type, "redeem");
assert_eq!(history[0].new_principal, Some(10_000));
let active = get_active_loans(pool).await.unwrap();
assert_eq!(
active.len(),
1,
"partial redeem must keep the continuation UTXO"
);
assert_eq!(active[0].principal, 10_000);
assert_eq!(active[0].txid, tx.compute_txid().to_string());
}
#[rocket::async_test]
async fn full_repay_drops_loan_utxo() {
let db = crate::utiltest::mock_db_pool(setup).await;
let pool = &db.moria_w;
let ids = token_ids();
let bh = blockhash(0xE3);
let seed = Txid::from_byte_array([0xA0; 32]);
insert_loan_utxo(
pool,
&seed,
2,
&LoanCommitmentV11 {
borrower_nft_hash: borrower(),
delegate_nft_hash: delegate(),
principal: 20_000,
annual_interest_rate_bp: 500,
timestamp: 1_700_000_000,
},
9_000_000,
&bh,
)
.await
.unwrap();
let tx = make_tx(
vec![
txin(0x01, 0, unlock(MORIA_METHOD_REPAY, &[])),
txin(0x02, 0, unlock(0, &[])),
txin(0xA0, 2, unlock(LOAN_METHOD_REPAY, &[])),
txin(0xB0, 0, ScriptBuf::new()),
],
vec![
out(1_000, Some(minting(ids.moria, vec![0x02], 1_100_000))),
out(1_000, Some(nft(tid(0xD1), vec![0x11; 16]))),
out(1_000, Some(ft(ids.moria, 42))),
bare(1_000),
bare(5_000_000),
bare(1_000),
],
);
index_moria_v11(pool, &[tx], &bh, 1_700_100_000, &ids)
.await
.unwrap();
assert!(get_active_loans(pool).await.unwrap().is_empty());
}
#[rocket::async_test]
async fn principal_128000_fits_loan_utxo() {
let db = crate::utiltest::mock_db_pool(setup).await;
let pool = &db.moria_w;
let bh = blockhash(0xE4);
insert_loan_utxo(
pool,
&Txid::from_byte_array([0xC8; 32]),
2,
&LoanCommitmentV11 {
borrower_nft_hash: borrower(),
delegate_nft_hash: delegate(),
principal: 128_000,
annual_interest_rate_bp: 500,
timestamp: 1_700_000_000,
},
50_000_000,
&bh,
)
.await
.unwrap();
let active = get_active_loans(pool).await.unwrap();
assert_eq!(active.len(), 1);
assert_eq!(active[0].principal, 128_000);
}