From bc31e722cc7ccb0540a071a1941b93aae65a80eb Mon Sep 17 00:00:00 2001 From: Quimey Lucas Marquez Date: Sun, 22 Mar 2026 11:34:45 -0300 Subject: [PATCH 1/8] =?UTF-8?q?feat(lazer/cardano):=20Factura=20Ya=20?= =?UTF-8?q?=E2=80=94=20invoice=20factoring=20marketplace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hackathon submission for the Cardano Pythathon. Factura Ya is an on-chain invoice factoring marketplace on Cardano. SMEs tokenize outstanding invoices as NFTs, deposit collateral, and sell collection rights to investors at a discount. Pyth's ADA/USD price feed provides real-time currency conversion. Co-Authored-By: Claude Opus 4.6 (1M context) --- lazer/cardano/factura-ya/README.md | 137 +++++++ lazer/cardano/factura-ya/aiken.toml | 19 + .../factura-ya/contracts-lib/parser.ak | 87 +++++ .../cardano/factura-ya/contracts-lib/pyth.ak | 231 ++++++++++++ .../factura-ya/contracts-lib/pyth/message.ak | 141 ++++++++ .../factura-ya/contracts-lib/pyth_oracle.ak | 147 ++++++++ .../cardano/factura-ya/contracts-lib/types.ak | 87 +++++ .../factura-ya/contracts-lib/types/i16.ak | 52 +++ .../factura-ya/contracts-lib/types/i64.ak | 52 +++ .../factura-ya/contracts-lib/types/u16.ak | 33 ++ .../factura-ya/contracts-lib/types/u32.ak | 33 ++ .../factura-ya/contracts-lib/types/u64.ak | 33 ++ .../factura-ya/contracts-lib/types/u8.ak | 28 ++ .../factura-ya/contracts-validators/escrow.ak | 103 ++++++ .../contracts-validators/invoice_mint.ak | 156 ++++++++ .../contracts-validators/marketplace.ak | 160 +++++++++ lazer/cardano/factura-ya/frontend/index.html | 12 + .../cardano/factura-ya/frontend/package.json | 22 ++ lazer/cardano/factura-ya/frontend/src/App.tsx | 55 +++ .../frontend/src/components/Marketplace.tsx | 95 +++++ .../frontend/src/components/PriceDisplay.tsx | 28 ++ .../src/components/RegisterInvoice.tsx | 142 ++++++++ .../frontend/src/components/WalletConnect.tsx | 90 +++++ .../cardano/factura-ya/frontend/src/index.css | 149 ++++++++ .../factura-ya/frontend/src/lib/api.ts | 29 ++ .../frontend/src/lib/transactions.ts | 122 +++++++ .../factura-ya/frontend/src/lib/wallet.ts | 136 +++++++ .../cardano/factura-ya/frontend/src/main.tsx | 10 + .../cardano/factura-ya/frontend/tsconfig.json | 19 + .../factura-ya/frontend/vite.config.ts | 15 + lazer/cardano/factura-ya/indexer/oura.toml | 31 ++ lazer/cardano/factura-ya/indexer/package.json | 20 ++ lazer/cardano/factura-ya/indexer/src/api.ts | 176 +++++++++ .../cardano/factura-ya/indexer/tsconfig.json | 14 + .../cardano/factura-ya/offchain/package.json | 18 + .../cardano/factura-ya/offchain/src/escrow.ts | 139 +++++++ .../factura-ya/offchain/src/marketplace.ts | 170 +++++++++ lazer/cardano/factura-ya/offchain/src/mint.ts | 117 ++++++ lazer/cardano/factura-ya/offchain/src/pyth.ts | 182 ++++++++++ .../cardano/factura-ya/offchain/tsconfig.json | 14 + lazer/cardano/factura-ya/plutus.json | 340 ++++++++++++++++++ 41 files changed, 3644 insertions(+) create mode 100644 lazer/cardano/factura-ya/README.md create mode 100644 lazer/cardano/factura-ya/aiken.toml create mode 100644 lazer/cardano/factura-ya/contracts-lib/parser.ak create mode 100644 lazer/cardano/factura-ya/contracts-lib/pyth.ak create mode 100644 lazer/cardano/factura-ya/contracts-lib/pyth/message.ak create mode 100644 lazer/cardano/factura-ya/contracts-lib/pyth_oracle.ak create mode 100644 lazer/cardano/factura-ya/contracts-lib/types.ak create mode 100644 lazer/cardano/factura-ya/contracts-lib/types/i16.ak create mode 100644 lazer/cardano/factura-ya/contracts-lib/types/i64.ak create mode 100644 lazer/cardano/factura-ya/contracts-lib/types/u16.ak create mode 100644 lazer/cardano/factura-ya/contracts-lib/types/u32.ak create mode 100644 lazer/cardano/factura-ya/contracts-lib/types/u64.ak create mode 100644 lazer/cardano/factura-ya/contracts-lib/types/u8.ak create mode 100644 lazer/cardano/factura-ya/contracts-validators/escrow.ak create mode 100644 lazer/cardano/factura-ya/contracts-validators/invoice_mint.ak create mode 100644 lazer/cardano/factura-ya/contracts-validators/marketplace.ak create mode 100644 lazer/cardano/factura-ya/frontend/index.html create mode 100644 lazer/cardano/factura-ya/frontend/package.json create mode 100644 lazer/cardano/factura-ya/frontend/src/App.tsx create mode 100644 lazer/cardano/factura-ya/frontend/src/components/Marketplace.tsx create mode 100644 lazer/cardano/factura-ya/frontend/src/components/PriceDisplay.tsx create mode 100644 lazer/cardano/factura-ya/frontend/src/components/RegisterInvoice.tsx create mode 100644 lazer/cardano/factura-ya/frontend/src/components/WalletConnect.tsx create mode 100644 lazer/cardano/factura-ya/frontend/src/index.css create mode 100644 lazer/cardano/factura-ya/frontend/src/lib/api.ts create mode 100644 lazer/cardano/factura-ya/frontend/src/lib/transactions.ts create mode 100644 lazer/cardano/factura-ya/frontend/src/lib/wallet.ts create mode 100644 lazer/cardano/factura-ya/frontend/src/main.tsx create mode 100644 lazer/cardano/factura-ya/frontend/tsconfig.json create mode 100644 lazer/cardano/factura-ya/frontend/vite.config.ts create mode 100644 lazer/cardano/factura-ya/indexer/oura.toml create mode 100644 lazer/cardano/factura-ya/indexer/package.json create mode 100644 lazer/cardano/factura-ya/indexer/src/api.ts create mode 100644 lazer/cardano/factura-ya/indexer/tsconfig.json create mode 100644 lazer/cardano/factura-ya/offchain/package.json create mode 100644 lazer/cardano/factura-ya/offchain/src/escrow.ts create mode 100644 lazer/cardano/factura-ya/offchain/src/marketplace.ts create mode 100644 lazer/cardano/factura-ya/offchain/src/mint.ts create mode 100644 lazer/cardano/factura-ya/offchain/src/pyth.ts create mode 100644 lazer/cardano/factura-ya/offchain/tsconfig.json create mode 100644 lazer/cardano/factura-ya/plutus.json diff --git a/lazer/cardano/factura-ya/README.md b/lazer/cardano/factura-ya/README.md new file mode 100644 index 00000000..e9f0229d --- /dev/null +++ b/lazer/cardano/factura-ya/README.md @@ -0,0 +1,137 @@ +# Team Facturas Ya Pythathon Submission + +## Details + +Team Name: Facturas Ya +Submission Name: Factura Ya +Team Members: Dario Fasolino, Macarena Carabajal +Contact: dario.a.fasolino@gmail.com, macacarabajal3@gmail.com + +## Project Description + +**Factura Ya** is an on-chain invoice factoring marketplace built on Cardano. It lets Latin American SMEs tokenize their outstanding invoices as NFTs and sell them at a discount to investors, providing immediate liquidity without banks or traditional factoring intermediaries. + +### The Problem + +SMEs in Argentina wait 60-90 days to collect on invoices. Traditional factoring charges 3-5% monthly with slow approvals. Meanwhile, $24B+ in tokenized real-world assets (RWAs) exist on-chain, but none serve LATAM SMEs. + +### How It Works + +1. **SME registers an invoice** — amount (ARS), due date, debtor info +2. **Invoice is tokenized** as an NFT on Cardano, with collateral locked in escrow (10-20%) +3. **Listed on the marketplace** with a discount and due date +4. **Investor purchases** — pays ADA, SME receives funds instantly +5. **At maturity** — investor confirms payment received (collateral returned to SME) or reports non-payment (collateral transferred to investor) + +### How We Use Pyth + +Pyth is **central** to the invoice valuation pipeline: + +- **On-chain**: The `pyth_oracle.ak` module calls `pyth.get_updates()` to read the verified ADA/USD price feed (feed ID 16) from the Pyth withdraw-script redeemer. Price freshness is validated (max 60 seconds). +- **Conversion**: `usd_to_lovelace()` converts invoice values from ARS/USD to ADA using the real-time Pyth price, enabling accurate marketplace pricing. +- **Listing**: When an invoice is listed, the current Pyth price is snapshotted into the listing datum as a reference. +- **Off-chain**: `PythPriceClient` subscribes to Pyth Pro WebSocket for live price updates, feeding them into transaction construction via the official `@pythnetwork/pyth-lazer-cardano-js` SDK. + +**PreProd Policy ID**: `d799d287105dea9377cdf9ea8502a83d2b9eb2d2050a8aea800a21e6` + +## Architecture + +``` +Frontend (React) ←→ Indexer API (Express) + ↓ ↑ +Off-chain Tx Builders Oura Pipeline + (TypeScript) ↑ + ↓ Cardano PreProd +┌──────────────────────────────────┐ +│ Smart Contracts (Aiken) │ +│ │ +│ invoice_mint.ak - NFT minting │ +│ escrow.ak - collateral │ +│ marketplace.ak - list/buy │ +│ pyth_oracle.ak - price feed │ +│ │ +│ Pyth Oracle (ADA/USD) │ +│ (withdraw-script pattern) │ +└──────────────────────────────────┘ +``` + +## Tech Stack + +| Component | Technology | +|-----------|-----------| +| Smart Contracts | Aiken (Plutus V3) | +| Oracle | Pyth Pro (feed 16: ADA/USD) | +| On-chain SDK | pyth-network/pyth-lazer-cardano | +| Off-chain SDK | @pythnetwork/pyth-lazer-cardano-js | +| Tx Builder | Evolution SDK | +| Indexer | Oura (TxPipe) + Express | +| Frontend | React + Vite + TypeScript | +| Network | Cardano PreProd | + +## Project Structure + +``` +factura_ya/ +├── contracts/ # Aiken smart contracts +│ ├── lib/ +│ │ ├── pyth_oracle.ak # Pyth price feed consumer +│ │ ├── types.ak # Shared types +│ │ ├── pyth.ak # Vendored Pyth library +│ │ ├── pyth/message.ak # Pyth message verification +│ │ ├── parser.ak # Binary parser +│ │ └── types/ # Integer types (u8..u64, i16, i64) +│ └── validators/ +│ ├── invoice_mint.ak # Invoice NFT minting policy +│ ├── escrow.ak # Collateral lock/release/forfeit +│ └── marketplace.ak # List/purchase/delist +├── offchain/ # TypeScript tx builders +│ └── src/ +│ ├── pyth.ts # Pyth WebSocket client +│ ├── mint.ts # Mint/burn invoice NFTs +│ ├── escrow.ts # Collateral operations +│ └── marketplace.ts # List/purchase/delist +├── indexer/ # Oura-powered marketplace indexer +│ ├── oura.toml # Pipeline config +│ └── src/api.ts # REST API +├── frontend/ # React web UI +│ └── src/ +│ ├── App.tsx +│ └── components/ # Marketplace, RegisterInvoice, PriceDisplay +└── custom_docs/ # Design docs (PRD, spec, Pyth research) +``` + +## Setup & Run + +### Prerequisites + +- [Aiken](https://aiken-lang.org/) v1.1.21+ +- Node.js 18+ +- Pyth API key (from hackathon organizers) + +### Build Contracts + +```bash +cd contracts +aiken build +aiken check # runs 25 tests +``` + +### Run Indexer + +```bash +cd indexer +npm install +npm start # starts on port 3001 +``` + +### Run Frontend + +```bash +cd frontend +npm install +npm run dev # starts on port 5173, proxies /api to indexer +``` + +## License + +Apache-2.0 diff --git a/lazer/cardano/factura-ya/aiken.toml b/lazer/cardano/factura-ya/aiken.toml new file mode 100644 index 00000000..cdef2280 --- /dev/null +++ b/lazer/cardano/factura-ya/aiken.toml @@ -0,0 +1,19 @@ +name = "factura-ya/contracts" +version = "0.0.0" +compiler = "v1.1.21" +plutus = "v3" +license = "Apache-2.0" +description = "Aiken contracts for project 'factura-ya/contracts'" + +[repository] +user = "factura-ya" +project = "contracts" +platform = "github" + +[[dependencies]] +name = "aiken-lang/stdlib" +version = "v3.0.0" +source = "github" + + +[config] diff --git a/lazer/cardano/factura-ya/contracts-lib/parser.ak b/lazer/cardano/factura-ya/contracts-lib/parser.ak new file mode 100644 index 00000000..6f506f9e --- /dev/null +++ b/lazer/cardano/factura-ya/contracts-lib/parser.ak @@ -0,0 +1,87 @@ +use aiken/collection/list +use aiken/primitive/bytearray + +/// `Parser` is a "stateful" function that modifies source `ByteArray` while +/// producing value of type `a`. Parsers can be constructed with `parse` +/// function, existing parsing combinators and `return` wrapper using +/// backpassing syntax: +/// +/// ``` +/// use parser.{Parser, parse} +/// +/// { +/// let length <- parse(u32.from_be) +/// let items <- parse(parser.repeat(item_parser, u32.as_int(length))) +/// (length, items) |> parser.return +/// } +/// ``` +/// +/// Use `run` or `run_partial` to execute the final parser over `ByteArray`. +pub type Parser = + fn(ByteArray) -> (a, ByteArray) + +/// Runs supplied parser over source `ByteArray`. Requires parser to consume +/// the whole input. +pub fn run(self: Parser, src: ByteArray) -> a { + expect (a, #[]) = self(src) + a +} + +/// Runs supplied parser over source `ByteArray`, returning unconsumed input. +pub fn run_partial(self: Parser, src: ByteArray) -> (a, ByteArray) { + self(src) +} + +/// Applies function over result of the supplied `Parser`. +pub fn map(self: Parser, with: fn(a) -> b) -> Parser { + fn(bs) { + let (a, bs1) = self(bs) + (with(a), bs1) + } +} + +/// Creates `Parser` that does not consume any input, returning provided value. +/// Can be used to construct return value of a composite parser. +pub fn return(value: a) -> Parser { + fn(bs) { (value, bs) } +} + +/// Function for chaining multiple parsers together using backpassing syntax. +/// Feeds result of the first parser into the provided function, producing new +/// composite parser. See `Parser` type for example. +pub fn parse(parser: Parser, then: fn(a) -> Parser) -> Parser { + fn(bs) { + let (a, bs1) = parser(bs) + then(a)(bs1) + } +} + +/// Parses remaining input, returning it as `ByteArray`. +pub const rest: Parser = fn(bs) { (bs, #[]) } + +/// Parses exactly the specified amount of bytes as `ByteArray`. For parsing +/// inidividual bytes as `Byte`, use `types/u8.from_be`. +pub fn bytes(count: Int) -> Parser { + expect count >= 0 + fn(bs) { + expect bytearray.length(bs) >= count + (bytearray.take(bs, count), bytearray.drop(bs, count)) + } +} + +/// Repeatedly applies provided parser, `count` times, returning list of +/// parsed results. +pub fn repeat(self: Parser, count: Int) -> Parser> { + expect count >= 0 + repeat_(self, [], count) +} + +fn repeat_(self: Parser, prev: List, count: Int) -> Parser> { + when count is { + 0 -> list.reverse(prev) |> return + _ -> { + let a <- parse(self) + repeat_(self, list.push(prev, a), count - 1) + } + } +} diff --git a/lazer/cardano/factura-ya/contracts-lib/pyth.ak b/lazer/cardano/factura-ya/contracts-lib/pyth.ak new file mode 100644 index 00000000..a8f1d2ae --- /dev/null +++ b/lazer/cardano/factura-ya/contracts-lib/pyth.ak @@ -0,0 +1,231 @@ +use aiken/collection/list +use aiken/collection/pairs +use aiken/crypto.{ScriptHash} +use cardano/address.{Script} +use cardano/assets.{PolicyId} +use cardano/transaction.{InlineDatum, Transaction, ValidityRange, Withdraw} +use parser.{Parser, parse} +use pyth/message.{TrustedSigners} +use types/i16 +use types/i64 +use types/u16.{U16} +use types/u32.{U32} +use types/u64.{U64} +use types/u8.{U8} + +const price_update_magic_le = #"75d3c793" + +pub type Governance { + wormhole: PolicyId, + emitter_chain: Int, + emitter_address: ByteArray, + seen_sequence: Int, +} + +pub type Pyth { + governance: Governance, + trusted_signers: TrustedSigners, + deprecated_withdraw_scripts: Pairs, + withdraw_script: ScriptHash, +} + +pub type PriceUpdate { + timestamp_us: U64, + channel_id: U8, + feeds: List, +} + +pub fn get_updates(pyth_id: PolicyId, self: Transaction) -> List { + expect Some(state) = + list.find_map( + self.reference_inputs, + fn(input) { + if assets.has_nft_strict(input.output.value, pyth_id, "Pyth State") { + // state token outside of script address would imply a bug + expect Script(_) = input.output.address.payment_credential + expect InlineDatum(state) = input.output.datum + expect state: Pyth = state + Some(state) + } else { + None + } + }, + ) + expect Some(redeemer) = + pairs.get_first(self.redeemers, Withdraw(Script(state.withdraw_script))) + expect updates: List = redeemer + list.map( + updates, + fn(message) { + let message = message.parse_without_verification(message) + parse_update(message.payload) + }, + ) +} + +fn parse_update(payload: ByteArray) -> PriceUpdate { + parser.run(price_update, payload) +} + +pub type Feed { + feed_id: U32, + price: Option>, + best_bid_price: Option>, + best_ask_price: Option>, + publisher_count: Option, + exponent: Option, + confidence: Option>, + funding_rate: Option>, + funding_timestamp: Option>, + funding_rate_interval: Option>, + market_session: Option, + ema_price: Option>, + ema_confidence: Option>, + feed_update_timestamp: Option>, +} + +fn empty(feed_id: U32) -> Feed { + Feed { + feed_id, + price: None, + best_bid_price: None, + best_ask_price: None, + publisher_count: None, + exponent: None, + confidence: None, + funding_rate: None, + funding_timestamp: None, + funding_rate_interval: None, + market_session: None, + ema_price: None, + ema_confidence: None, + feed_update_timestamp: None, + } +} + +type FeedProperty { + Price(Option) + BestBidPrice(Option) + BestAskPrice(Option) + PublisherCount(U16) + Exponent(Int) + Confidence(Option) + FundingRate(Option) + FundingTimestamp(Option) + FundingRateInterval(Option) + MarketSession(MarketSession) + EmaPrice(Option) + EmaConfidence(Option) + FeedUpdateTimestamp(Option) +} + +fn apply_properties(self: Feed, properties: List) -> Feed { + list.foldl( + properties, + self, + fn(f, self) { + when f is { + Price(p) -> Feed { ..self, price: Some(p) } + BestBidPrice(p) -> Feed { ..self, best_bid_price: Some(p) } + BestAskPrice(p) -> Feed { ..self, best_ask_price: Some(p) } + PublisherCount(p) -> Feed { ..self, publisher_count: Some(p) } + Exponent(p) -> Feed { ..self, exponent: Some(p) } + Confidence(p) -> Feed { ..self, confidence: Some(p) } + FundingRate(p) -> Feed { ..self, funding_rate: Some(p) } + FundingTimestamp(p) -> Feed { ..self, funding_timestamp: Some(p) } + FundingRateInterval(p) -> + Feed { ..self, funding_rate_interval: Some(p) } + MarketSession(p) -> Feed { ..self, market_session: Some(p) } + EmaPrice(p) -> Feed { ..self, ema_price: Some(p) } + EmaConfidence(p) -> Feed { ..self, ema_confidence: Some(p) } + FeedUpdateTimestamp(p) -> + Feed { ..self, feed_update_timestamp: Some(p) } + } + }, + ) +} + +pub type MarketSession { + Regular + PreMarket + PostMarket + OverNight + Closed +} + +fn nonzero_i64_property( + property: fn(Option) -> FeedProperty, +) -> Parser { + let i <- parser.map(i64.from_le) + let i = i64.as_int(i) + property( + if i == 0 { + None + } else { + Some(i) + }, + ) +} + +fn optional_property( + property: fn(Option) -> FeedProperty, + parser: Parser, +) -> Parser { + let present <- parse(u8.from) + if present != u8.zero { + parser.map(parser, fn(p) { property(Some(p)) }) + } else { + property(None) |> parser.return + } +} + +fn market_session() -> Parser { + let s <- parser.map(u16.from_le) + when u16.as_int(s) is { + 0 -> Regular + 1 -> PreMarket + 2 -> PostMarket + 3 -> OverNight + 4 -> Closed + _ -> fail @"MarketSession value out of range" + } +} + +fn feed_property() -> Parser { + let property_id <- parse(u8.from) + when u8.as_int(property_id) is { + 0 -> nonzero_i64_property(Price) + 1 -> nonzero_i64_property(BestBidPrice) + 2 -> nonzero_i64_property(BestAskPrice) + 3 -> parser.map(u16.from_le, PublisherCount) + 4 -> parser.map(i16.from_le, fn(i) { Exponent(i16.as_int(i)) }) + 5 -> nonzero_i64_property(Confidence) + 6 -> optional_property(FundingRate, parser.map(i64.from_le, i64.as_int)) + 7 -> optional_property(FundingTimestamp, u64.from_le) + 8 -> optional_property(FundingRateInterval, u64.from_le) + 9 -> parser.map(market_session(), MarketSession) + 10 -> nonzero_i64_property(EmaPrice) + 11 -> nonzero_i64_property(EmaConfidence) + 12 -> optional_property(FeedUpdateTimestamp, u64.from_le) + _ -> fail @"FeedProperty ID out of range" + } +} + +const feed: Parser = { + let feed_id <- parse(u32.from_le) + let properties_len <- parse(u8.from) + let properties <- + parse(parser.repeat(feed_property(), u8.as_int(properties_len))) + apply_properties(empty(feed_id), properties) |> parser.return + } + +const price_update: Parser = { + let magic <- parse(parser.bytes(4)) + expect magic == price_update_magic_le + let timestamp_us <- parse(u64.from_le) + let channel_id <- parse(u8.from) + let feeds_len <- parse(u8.from) + let feeds <- parse(parser.repeat(feed, u8.as_int(feeds_len))) + PriceUpdate { timestamp_us, channel_id, feeds } |> parser.return + } + diff --git a/lazer/cardano/factura-ya/contracts-lib/pyth/message.ak b/lazer/cardano/factura-ya/contracts-lib/pyth/message.ak new file mode 100644 index 00000000..05255d48 --- /dev/null +++ b/lazer/cardano/factura-ya/contracts-lib/pyth/message.ak @@ -0,0 +1,141 @@ +use aiken/collection/pairs +use aiken/crypto.{Signature, VerificationKey} +use aiken/interval.{Interval} +use aiken/option +use aiken/primitive/bytearray +use cardano/transaction.{ValidityRange} +use parser.{Parser, parse} +use types/u16 + +const solana_format_magic_le = #"b9011a82" + +pub type TrustedSigners = + Pairs + +pub fn is_trusted( + self: TrustedSigners, + key: VerificationKey, + current: ValidityRange, +) { + pairs.get_first(self, key) + |> option.map(fn(valid) { interval.includes(valid, current) }) + |> option.or_else(False) +} + +pub fn update_trusted_signer( + self: TrustedSigners, + key: VerificationKey, + valid: Option, +) { + when valid is { + None -> pairs.delete_all(self, key) + Some(valid) -> + pairs.repsert_by_ascending_key(self, key, valid, bytearray.compare) + } +} + +pub type PythMessage { + signature: Signature, + key: VerificationKey, + payload: ByteArray, +} + +pub fn parse_and_verify( + src: ByteArray, + signers: TrustedSigners, + current: ValidityRange, +) -> PythMessage { + let update = parse_without_verification(src) + expect + crypto.verify_ed25519_signature( + update.key, + update.payload, + update.signature, + )? + expect is_trusted(signers, update.key, current) + update +} + +pub fn parse_without_verification(src: ByteArray) -> PythMessage { + parser.run(pyth_message, src) +} + +const pyth_message: Parser = { + let magic <- parse(parser.bytes(4)) + expect magic == solana_format_magic_le + let signature <- parse(parser.bytes(64)) + let key <- parse(parser.bytes(32)) + let size <- parse(u16.from_le) + let payload <- parse(parser.bytes(u16.as_int(size))) + + PythMessage { signature, key, payload } |> parser.return + } + +const test_interval: Interval = interval.before(1) + +const test_signers: TrustedSigners = + [ + Pair( + #"74313a6525edf99936aa1477e94c72bc5cc617b21745f5f03296f3154461f214", + test_interval, + ), + ] + +test can_parse_message() { + let message = + #"b9011a82e5cddee2c1bd364c8c57e1c98a6a28d194afcad410ff412226c8b2ae931ff59a57147cb47c7307afc2a0a1abec4dd7e835a5b7113cf5aeac13a745c6bed6c60074313a6525edf99936aa1477e94c72bc5cc617b21745f5f03296f3154461f2141c0075d3c7931c9773f30a240600010102000000010000e1f50500000000" + + expect _ = parse_and_verify(message, test_signers, test_interval) +} + +test fails_with_wrong_signature() fail { + let message = + #"b9011a82e5cddee2c1bd364c8c57e1c98a6a28d194afcad410ff412226c8b2ae931ff59a57147cb47c7307afc2a0a1abec4dd7e835a5b7113cf5aeac13a745c6bed6c60074313a6525edf99936aa1477e94c72bc5cc617b21745f5f03296f3154461f2141c0075d3c7931c9773f30a240600010102000000010000e1f50500000001" + + expect _ = parse_and_verify(message, test_signers, test_interval) +} + +test fails_with_bad_magic() fail { + let message = + #"c9011a82e5cddee2c1bd364c8c57e1c98a6a28d194afcad410ff412226c8b2ae931ff59a57147cb47c7307afc2a0a1abec4dd7e835a5b7113cf5aeac13a745c6bed6c60074313a6525edf99936aa1477e94c72bc5cc617b21745f5f03296f3154461f2141c0075d3c7931c9773f30a240600010102000000010000e1f50500000000" + + expect _ = parse_and_verify(message, test_signers, test_interval) +} + +test fails_with_bad_length() fail { + let message = #"c9011a82e5cddee2c1bd364c8c57e1" + + expect _ = parse_and_verify(message, test_signers, test_interval) +} + +test bad_payload_length_too_short() fail { + let message = + #"b9011a82e5cddee2c1bd364c8c57e1c98a6a28d194afcad410ff412226c8b2ae931ff59a57147cb47c7307afc2a0a1abec4dd7e835a5b7113cf5aeac13a745c6bed6c60074313a6525edf99936aa1477e94c72bc5cc617b21745f5f03296f3154461f2141c0075d3c7931c9773f30a240600010102000000010000e1f505000000" + + expect _ = parse_and_verify(message, test_signers, test_interval) +} + +test bad_payload_length_too_long() fail { + let message = + #"b9011a82e5cddee2c1bd364c8c57e1c98a6a28d194afcad410ff412226c8b2ae931ff59a57147cb47c7307afc2a0a1abec4dd7e835a5b7113cf5aeac13a745c6bed6c60074313a6525edf99936aa1477e94c72bc5cc617b21745f5f03296f3154461f2141c0075d3c7931c9773f30a240600010102000000010000e1f5050000000000" + + expect _ = parse_and_verify(message, test_signers, test_interval) +} + +test adds_new_signer() { + update_trusted_signer([], #"", Some(interval.before(10))) == [ + Pair(#"", interval.before(10)), + ] +} + +test removes_signer() { + update_trusted_signer([Pair(#"", interval.before(10))], #"", None) == [] +} + +test updates_signer() { + update_trusted_signer( + [Pair(#"aa", interval.before(42))], + #"bb", + Some(interval.before(12)), + ) == [Pair(#"aa", interval.before(42)), Pair(#"bb", interval.before(12))] +} diff --git a/lazer/cardano/factura-ya/contracts-lib/pyth_oracle.ak b/lazer/cardano/factura-ya/contracts-lib/pyth_oracle.ak new file mode 100644 index 00000000..045075f5 --- /dev/null +++ b/lazer/cardano/factura-ya/contracts-lib/pyth_oracle.ak @@ -0,0 +1,147 @@ +use aiken/collection/list +use cardano/assets.{PolicyId} +use cardano/transaction.{Transaction} +use pyth +use types.{PriceData} +use types/u32.{U32} +use types/u64 + +/// Maximum age of a price update in microseconds (60 seconds). +pub const max_age_us: Int = 60_000_000 + +/// Extract the price for a specific feed from a Pyth price update within a transaction. +/// +/// Calls `pyth.get_updates` to retrieve all verified price updates from the +/// Pyth withdraw-script redeemer, then finds the feed matching `target_feed_id`. +/// +/// Fails if: +/// - No Pyth updates are present in the transaction +/// - The target feed is not found in any update +/// - The feed has no price or exponent +/// - The price update is stale (older than `max_age_us` relative to `current_time_us`) +pub fn get_price( + pyth_id: PolicyId, + tx: Transaction, + target_feed_id: U32, + current_time_us: Int, +) -> PriceData { + let updates = pyth.get_updates(pyth_id, tx) + + expect Some(found) = + list.find_map( + updates, + fn(update) { + list.find( + update.feeds, + fn(feed) { feed.feed_id == target_feed_id }, + ) + |> option_map(fn(feed) { (update.timestamp_us, feed) }) + }, + ) + + let (timestamp_us, feed) = found + + // Validate freshness + let age_us = current_time_us - u64.as_int(timestamp_us) + expect age_us >= 0 + expect age_us <= max_age_us + + // Extract price and exponent (must be present) + expect Some(Some(price)) = feed.price + expect Some(exponent) = feed.exponent + + PriceData { + feed_id: feed.feed_id, + price, + exponent, + timestamp_us, + } +} + +/// Convert a USD amount to lovelace using an ADA/USD price. +/// +/// ADA/USD price means: 1 ADA = (ada_usd_price * 10^exponent) USD +/// lovelace = (usd_amount * 10^(6 - exponent)) / ada_usd_price +/// +/// For the MVP we treat ARS ≈ USD. With a real ARS/USD feed, +/// first convert ARS→USD, then use this function for USD→ADA. +pub fn usd_to_lovelace( + usd_amount: Int, + ada_usd_price: Int, + ada_usd_exponent: Int, +) -> Int { + let power = 6 - ada_usd_exponent + let numerator = usd_amount * pow10(power) + numerator / ada_usd_price +} + +/// Integer power of 10. Only supports non-negative exponents. +pub fn pow10(n: Int) -> Int { + do_pow10(n, 1) +} + +fn do_pow10(n: Int, acc: Int) -> Int { + if n <= 0 { + acc + } else { + do_pow10(n - 1, acc * 10) + } +} + +// --- Helpers --- + +fn option_map(opt: Option, f: fn(a) -> b) -> Option { + when opt is { + Some(x) -> Some(f(x)) + None -> None + } +} + +// --- Tests --- + +test pow10_zero() { + pow10(0) == 1 +} + +test pow10_positive() { + pow10(8) == 100_000_000 +} + +test usd_to_lovelace_basic() { + // 1 ADA = $0.50 (price = 50000000, exponent = -8) + // $100 USD → 200 ADA → 200_000_000 lovelace + usd_to_lovelace(100, 50_000_000, -8) == 200_000_000 +} + +test usd_to_lovelace_realistic() { + // 1 ADA = $0.695087 (price = 69508700, exponent = -8) + // $1000 USD → ~1438.68 ADA → 1_438_676_157 lovelace (truncated) + let result = usd_to_lovelace(1000, 69_508_700, -8) + result > 1_400_000_000 && result < 1_500_000_000 +} + +test usd_to_lovelace_one_dollar() { + // 1 ADA = $1.00 (price = 100000000, exponent = -8) + // $1 USD → 1 ADA → 1_000_000 lovelace + usd_to_lovelace(1, 100_000_000, -8) == 1_000_000 +} + +test usd_to_lovelace_small_amount() { + // 1 ADA = $0.50, $10 → 20 ADA → 20_000_000 lovelace + usd_to_lovelace(10, 50_000_000, -8) == 20_000_000 +} + +test usd_to_lovelace_large_invoice() { + // 1 ADA = $0.70 (price = 70000000, exponent = -8) + // $100_000 USD → ~142857 ADA + let result = usd_to_lovelace(100_000, 70_000_000, -8) + result > 142_000_000_000 && result < 143_000_000_000 +} + +test max_age_is_60_seconds() { + max_age_us == 60_000_000 +} + +test pow10_large() { + pow10(14) == 100_000_000_000_000 +} diff --git a/lazer/cardano/factura-ya/contracts-lib/types.ak b/lazer/cardano/factura-ya/contracts-lib/types.ak new file mode 100644 index 00000000..b49a21f8 --- /dev/null +++ b/lazer/cardano/factura-ya/contracts-lib/types.ak @@ -0,0 +1,87 @@ +use cardano/assets.{PolicyId} +use aiken/crypto.{VerificationKeyHash} +use types/u32.{U32} +use types/u64.{U64} + +// --- Price types (Block 1) --- + +/// Validated price extracted from a Pyth feed. +/// `price` and `exponent` together represent the real value: +/// real_price = price * 10^exponent +pub type PriceData { + feed_id: U32, + price: Int, + exponent: Int, + timestamp_us: U64, +} + +// --- Invoice types (Block 2) --- + +pub type InvoiceMintRedeemer { + MintInvoice + BurnInvoice +} + +pub type InvoiceDatum { + invoice_id: ByteArray, + seller: VerificationKeyHash, + amount_ars: Int, + due_date_posix_ms: Int, + debtor_name: ByteArray, + debtor_contact_hash: ByteArray, + created_at_posix_ms: Int, +} + +// --- Escrow types (Block 3) --- + +pub type EscrowStatus { + Locked + Released + Forfeited +} + +pub type EscrowDatum { + invoice_id: ByteArray, + seller: VerificationKeyHash, + collateral_lovelace: Int, + buyer: Option, + due_date_posix_ms: Int, + status: EscrowStatus, +} + +pub type EscrowRedeemer { + Release + Forfeit +} + +// --- Marketplace types (Block 4) --- + +pub type ListingStatus { + Listed + Sold + Delisted +} + +pub type ListingDatum { + invoice_id: ByteArray, + seller: VerificationKeyHash, + asking_price_lovelace: Int, + original_amount_ars: Int, + pyth_price_at_listing: Int, + pyth_exponent_at_listing: Int, + due_date_posix_ms: Int, + status: ListingStatus, +} + +pub type MarketplaceRedeemer { + Purchase + Delist +} + +// --- Config --- + +/// Pyth deployment config passed as a validator parameter. +pub type PythConfig { + pyth_policy_id: PolicyId, + ada_usd_feed_id: U32, +} diff --git a/lazer/cardano/factura-ya/contracts-lib/types/i16.ak b/lazer/cardano/factura-ya/contracts-lib/types/i16.ak new file mode 100644 index 00000000..acc4bf39 --- /dev/null +++ b/lazer/cardano/factura-ya/contracts-lib/types/i16.ak @@ -0,0 +1,52 @@ +use aiken/math +use aiken/primitive/int +use parser.{Parser} + +pub opaque type I16 { + I16(Int) +} + +pub const zero: I16 = I16(0) + +const min: Int = math.pow2(15) + +pub const from_be: Parser = + parser.bytes(2) + |> parser.map(fn(bs) { I16(int.from_bytearray_big_endian(bs)) }) + +pub const from_le: Parser = + parser.bytes(2) + |> parser.map(fn(bs) { I16(int.from_bytearray_little_endian(bs)) }) + +pub fn as_int(I16(self): I16) -> Int { + if self < min { + self + } else { + self - math.pow2(16) + } +} + +test parses_zero() { + expect as_int(parser.run(from_be, #"0000")) == 0 +} + +test parses_one() { + expect as_int(parser.run(from_be, #"0001")) == 1 +} + +test parses_max() { + expect as_int(parser.run(from_be, #"7fff")) == math.pow2(15) - 1 +} + +test parses_min() { + let i = parser.run(from_be, #"8000") + expect as_int(i) == -math.pow2(15) +} + +test parses_minus_one() { + expect as_int(parser.run(from_be, #"ffff")) == -1 +} + +test parses_minus_two() { + expect as_int(parser.run(from_be, #"fffe")) == -2 +} diff --git a/lazer/cardano/factura-ya/contracts-lib/types/i64.ak b/lazer/cardano/factura-ya/contracts-lib/types/i64.ak new file mode 100644 index 00000000..b37fdb55 --- /dev/null +++ b/lazer/cardano/factura-ya/contracts-lib/types/i64.ak @@ -0,0 +1,52 @@ +use aiken/math +use aiken/primitive/int +use parser.{Parser} + +pub opaque type I64 { + I64(Int) +} + +pub const zero: I64 = I64(0) + +const min: Int = math.pow2(63) + +pub const from_be: Parser = + parser.bytes(8) + |> parser.map(fn(bs) { I64(int.from_bytearray_big_endian(bs)) }) + +pub const from_le: Parser = + parser.bytes(8) + |> parser.map(fn(bs) { I64(int.from_bytearray_little_endian(bs)) }) + +pub fn as_int(I64(self): I64) -> Int { + if self < min { + self + } else { + self - math.pow2(64) + } +} + +test parses_zero() { + expect as_int(parser.run(from_be, #"0000000000000000")) == 0 +} + +test parses_one() { + expect as_int(parser.run(from_be, #"0000000000000001")) == 1 +} + +test parses_max() { + expect as_int(parser.run(from_be, #"7fffffffffffffff")) == math.pow2(63) - 1 +} + +test parses_min() { + let i = parser.run(from_be, #"8000000000000000") + expect as_int(i) == -math.pow2(63) +} + +test parses_minus_one() { + expect as_int(parser.run(from_be, #"ffffffffffffffff")) == -1 +} + +test parses_minus_two() { + expect as_int(parser.run(from_be, #"fffffffffffffffe")) == -2 +} diff --git a/lazer/cardano/factura-ya/contracts-lib/types/u16.ak b/lazer/cardano/factura-ya/contracts-lib/types/u16.ak new file mode 100644 index 00000000..3a7ca74c --- /dev/null +++ b/lazer/cardano/factura-ya/contracts-lib/types/u16.ak @@ -0,0 +1,33 @@ +use aiken/math +use aiken/primitive/bytearray +use aiken/primitive/int +use parser.{Parser} + +pub opaque type U16 { + U16(Int) +} + +pub const zero: U16 = U16(0) + +pub const max: Int = math.pow2(16) - 1 + +pub fn as_int(U16(self): U16) -> Int { + self +} + +pub fn from_int(value: Int) -> U16 { + expect value >= 0 && value <= max + U16(value) +} + +pub const from_be: Parser = + parser.bytes(2) + |> parser.map(fn(bs) { U16(int.from_bytearray_big_endian(bs)) }) + +pub const from_le: Parser = + parser.bytes(2) + |> parser.map(fn(bs) { U16(int.from_bytearray_little_endian(bs)) }) + +pub fn to_be(self: U16) -> ByteArray { + bytearray.from_int_big_endian(as_int(self), 2) +} diff --git a/lazer/cardano/factura-ya/contracts-lib/types/u32.ak b/lazer/cardano/factura-ya/contracts-lib/types/u32.ak new file mode 100644 index 00000000..8fdf7a6a --- /dev/null +++ b/lazer/cardano/factura-ya/contracts-lib/types/u32.ak @@ -0,0 +1,33 @@ +use aiken/math +use aiken/primitive/bytearray +use aiken/primitive/int +use parser.{Parser} + +pub opaque type U32 { + U32(Int) +} + +pub const zero: U32 = U32(0) + +pub const max: Int = math.pow2(32) - 1 + +pub fn as_int(U32(self): U32) -> Int { + self +} + +pub fn from_int(value: Int) -> U32 { + expect value >= 0 && value <= max + U32(value) +} + +pub const from_be: Parser = + parser.bytes(4) + |> parser.map(fn(bs) { U32(int.from_bytearray_big_endian(bs)) }) + +pub const from_le: Parser = + parser.bytes(4) + |> parser.map(fn(bs) { U32(int.from_bytearray_little_endian(bs)) }) + +pub fn to_be(self: U32) -> ByteArray { + bytearray.from_int_big_endian(as_int(self), 4) +} diff --git a/lazer/cardano/factura-ya/contracts-lib/types/u64.ak b/lazer/cardano/factura-ya/contracts-lib/types/u64.ak new file mode 100644 index 00000000..40d9c0da --- /dev/null +++ b/lazer/cardano/factura-ya/contracts-lib/types/u64.ak @@ -0,0 +1,33 @@ +use aiken/math +use aiken/primitive/bytearray +use aiken/primitive/int +use parser.{Parser} + +pub opaque type U64 { + U64(Int) +} + +pub const zero: U64 = U64(0) + +pub const max: Int = math.pow2(64) - 1 + +pub fn as_int(U64(self): U64) -> Int { + self +} + +pub fn from_int(value: Int) -> U64 { + expect value >= 0 && value <= max + U64(value) +} + +pub const from_be: Parser = + parser.bytes(8) + |> parser.map(fn(bs) { U64(int.from_bytearray_big_endian(bs)) }) + +pub const from_le: Parser = + parser.bytes(8) + |> parser.map(fn(bs) { U64(int.from_bytearray_little_endian(bs)) }) + +pub fn to_be(self: U64) -> ByteArray { + bytearray.from_int_big_endian(as_int(self), 8) +} diff --git a/lazer/cardano/factura-ya/contracts-lib/types/u8.ak b/lazer/cardano/factura-ya/contracts-lib/types/u8.ak new file mode 100644 index 00000000..4fbdfeed --- /dev/null +++ b/lazer/cardano/factura-ya/contracts-lib/types/u8.ak @@ -0,0 +1,28 @@ +use aiken/math +use aiken/primitive/bytearray.{Byte} +use parser.{Parser} + +pub opaque type U8 { + U8(Int) +} + +pub const zero: U8 = U8(0) + +pub const max: Int = math.pow2(8) - 1 + +pub fn as_int(U8(self): U8) -> Byte { + self +} + +pub fn from_int(value: Int) -> U8 { + expect value >= 0 && value <= max + U8(value) +} + +pub const from: Parser = + fn(bs) { (U8(bytearray.at(bs, 0)), bytearray.drop(bs, 1)) } + +pub fn to_be(self: U8) -> ByteArray { + let U8(value) = self + bytearray.push(#[], value) +} diff --git a/lazer/cardano/factura-ya/contracts-validators/escrow.ak b/lazer/cardano/factura-ya/contracts-validators/escrow.ak new file mode 100644 index 00000000..a993a70e --- /dev/null +++ b/lazer/cardano/factura-ya/contracts-validators/escrow.ak @@ -0,0 +1,103 @@ +use aiken/collection/list +use aiken/interval.{Finite} +use cardano/address.{VerificationKey} +use cardano/assets +use cardano/transaction.{Transaction, InlineDatum, OutputReference} +use types.{ + EscrowDatum, EscrowRedeemer, + Locked, Released, Forfeited, + Release, Forfeit, +} + +/// Grace period after due date before forfeit is allowed (7 days in ms). +pub const grace_period_ms: Int = 7 * 24 * 60 * 60 * 1000 + +/// Minimum collateral percentage (basis points). 1000 = 10%. +pub const min_collateral_bps: Int = 1000 + +validator escrow { + spend( + datum: Option, + redeemer: EscrowRedeemer, + _utxo: OutputReference, + tx: Transaction, + ) { + expect Some(d) = datum + when redeemer is { + Release -> validate_release(d, tx) + Forfeit -> validate_forfeit(d, tx) + } + } + + else(_) { + fail + } +} + +// --- Release: buyer confirms payment received, collateral goes back to seller --- + +fn validate_release(datum: EscrowDatum, tx: Transaction) -> Bool { + expect datum.status == Locked + expect Some(buyer) = datum.buyer + expect list.has(tx.extra_signatories, buyer) + + let lower = get_validity_start(tx) + expect lower >= datum.due_date_posix_ms + + expect pays_to_pkh(tx, datum.seller, datum.collateral_lovelace) + + True +} + +// --- Forfeit: buyer reports non-payment, collateral goes to buyer --- + +fn validate_forfeit(datum: EscrowDatum, tx: Transaction) -> Bool { + expect datum.status == Locked + expect Some(buyer) = datum.buyer + expect list.has(tx.extra_signatories, buyer) + + let lower = get_validity_start(tx) + expect lower >= datum.due_date_posix_ms + grace_period_ms + + expect pays_to_pkh(tx, buyer, datum.collateral_lovelace) + + True +} + +// --- Helpers --- + +fn get_validity_start(tx: Transaction) -> Int { + when tx.validity_range.lower_bound.bound_type is { + Finite(time) -> time + _ -> 0 + } +} + +// --- Tests --- + +test grace_period_is_7_days() { + grace_period_ms == 7 * 24 * 60 * 60 * 1000 +} + +test min_collateral_is_10_percent() { + min_collateral_bps == 1000 +} + +// --- Helpers --- + +fn pays_to_pkh( + tx: Transaction, + pkh: ByteArray, + min_lovelace: Int, +) -> Bool { + list.any( + tx.outputs, + fn(o) { + when o.address.payment_credential is { + VerificationKey(hash) -> + hash == pkh && assets.lovelace_of(o.value) >= min_lovelace + _ -> False + } + }, + ) +} diff --git a/lazer/cardano/factura-ya/contracts-validators/invoice_mint.ak b/lazer/cardano/factura-ya/contracts-validators/invoice_mint.ak new file mode 100644 index 00000000..3ded5a81 --- /dev/null +++ b/lazer/cardano/factura-ya/contracts-validators/invoice_mint.ak @@ -0,0 +1,156 @@ +use aiken/collection/list +use aiken/collection/dict +use aiken/interval.{Finite} +use cardano/address.{Script} +use cardano/assets.{PolicyId} +use cardano/transaction.{Transaction, InlineDatum, Output} +use types.{ + InvoiceMintRedeemer, MintInvoice, BurnInvoice, + InvoiceDatum, + EscrowDatum, Released, Forfeited, +} + +/// Minting policy for invoice NFTs. +/// +/// `escrow_script_hash`: the hash of the escrow validator, used to find +/// escrow UTxOs when validating burns. +validator invoice_mint(escrow_script_hash: ByteArray) { + mint( + redeemer: InvoiceMintRedeemer, + policy_id: PolicyId, + tx: Transaction, + ) { + when redeemer is { + MintInvoice -> validate_mint(policy_id, tx) + BurnInvoice -> validate_burn(policy_id, escrow_script_hash, tx) + } + } + + else(_) { + fail + } +} + +// --- Mint validation --- + +fn validate_mint(policy_id: PolicyId, tx: Transaction) -> Bool { + // Exactly one token minted under this policy + let minted = assets.tokens(tx.mint, policy_id) + expect [Pair(invoice_id, quantity)] = dict.to_pairs(minted) + expect quantity == 1 + + // Find the output carrying the minted NFT with an InvoiceDatum + let datum = find_invoice_output(tx.outputs, policy_id, invoice_id) + + // Seller must sign the transaction + expect list.has(tx.extra_signatories, datum.seller) + + // Amount must be positive + expect datum.amount_ars > 0 + + // Due date must be in the future (after tx validity range start) + let lower_bound = get_validity_start(tx) + expect datum.due_date_posix_ms > lower_bound + + // Debtor contact hash must not be empty + expect datum.debtor_contact_hash != #"" + + // Invoice ID in datum must match asset name + expect datum.invoice_id == invoice_id + + True +} + +// --- Burn validation --- + +fn validate_burn( + policy_id: PolicyId, + escrow_script_hash: ByteArray, + tx: Transaction, +) -> Bool { + // Exactly one token burned (quantity = -1) + let minted = assets.tokens(tx.mint, policy_id) + expect [Pair(invoice_id, quantity)] = dict.to_pairs(minted) + expect quantity == -1 + + // Find the escrow UTxO for this invoice + let escrow = find_escrow_datum(tx, escrow_script_hash, invoice_id) + + // Escrow must be in a terminal state + expect escrow.status == Released || escrow.status == Forfeited + + // The NFT holder must sign + when escrow.buyer is { + Some(buyer) -> list.has(tx.extra_signatories, buyer) + None -> list.has(tx.extra_signatories, escrow.seller) + } +} + +// --- Helpers --- + +fn find_invoice_output( + outputs: List, + policy_id: PolicyId, + invoice_id: ByteArray, +) -> InvoiceDatum { + expect Some(output) = + list.find( + outputs, + fn(o) { assets.quantity_of(o.value, policy_id, invoice_id) == 1 }, + ) + expect InlineDatum(raw) = output.datum + expect datum: InvoiceDatum = raw + datum +} + +fn find_escrow_datum( + tx: Transaction, + escrow_script_hash: ByteArray, + invoice_id: ByteArray, +) -> EscrowDatum { + let all_outputs = + list.concat( + list.map(tx.reference_inputs, fn(i) { i.output }), + list.map(tx.inputs, fn(i) { i.output }), + ) + expect Some(output) = + list.find( + all_outputs, + fn(o) { + is_escrow_for_invoice(o, escrow_script_hash, invoice_id) + }, + ) + expect InlineDatum(raw) = output.datum + expect datum: EscrowDatum = raw + datum +} + +fn is_escrow_for_invoice( + output: Output, + escrow_script_hash: ByteArray, + invoice_id: ByteArray, +) -> Bool { + when output.address.payment_credential is { + Script(hash) -> + if hash == escrow_script_hash { + when output.datum is { + InlineDatum(raw) -> { + expect datum: EscrowDatum = raw + datum.invoice_id == invoice_id + } + _ -> False + } + } else { + False + } + _ -> False + } +} + +fn get_validity_start(tx: Transaction) -> Int { + when tx.validity_range.lower_bound.bound_type is { + Finite(time) -> time + _ -> 0 + } +} + diff --git a/lazer/cardano/factura-ya/contracts-validators/marketplace.ak b/lazer/cardano/factura-ya/contracts-validators/marketplace.ak new file mode 100644 index 00000000..c2f73dc0 --- /dev/null +++ b/lazer/cardano/factura-ya/contracts-validators/marketplace.ak @@ -0,0 +1,160 @@ +use aiken/collection/list +use aiken/collection/dict +use cardano/address.{VerificationKey, Script} +use cardano/assets +use cardano/transaction.{Transaction, InlineDatum, OutputReference, Output} +use types.{ + ListingDatum, MarketplaceRedeemer, ListingStatus, + Listed, Sold, Delisted, + Purchase, Delist, + EscrowDatum, Locked, +} + +/// Marketplace validator for listing, purchasing, and delisting invoice NFTs. +/// +/// `invoice_policy_id`: the minting policy ID for invoice NFTs. +/// `escrow_script_hash`: the escrow validator hash (to find and update escrow UTxOs). +validator marketplace( + invoice_policy_id: ByteArray, + escrow_script_hash: ByteArray, +) { + spend( + datum: Option, + redeemer: MarketplaceRedeemer, + own_ref: OutputReference, + tx: Transaction, + ) { + expect Some(d) = datum + when redeemer is { + Purchase -> validate_purchase(d, invoice_policy_id, escrow_script_hash, tx) + Delist -> validate_delist(d, invoice_policy_id, tx) + } + } + + else(_) { + fail + } +} + +// --- Purchase: investor buys the invoice NFT --- + +fn validate_purchase( + listing: ListingDatum, + invoice_policy_id: ByteArray, + escrow_script_hash: ByteArray, + tx: Transaction, +) -> Bool { + // Must be currently listed + expect listing.status == Listed + + // Payment to seller must be >= asking price + expect + pays_to_pkh(tx, listing.seller, listing.asking_price_lovelace) + + // NFT must go to the buyer (someone who signed the tx, not the seller) + expect Some(buyer_pkh) = + list.find( + tx.extra_signatories, + fn(sig) { sig != listing.seller }, + ) + + expect + nft_goes_to_pkh(tx, invoice_policy_id, listing.invoice_id, buyer_pkh) + + // Escrow must be updated with the buyer's identity. + // We check that an output to the escrow script exists with matching + // invoice_id and buyer set to Some(buyer_pkh). + expect + escrow_output_has_buyer( + tx, escrow_script_hash, listing.invoice_id, buyer_pkh, + ) + + True +} + +// --- Delist: seller withdraws the listing --- + +fn validate_delist( + listing: ListingDatum, + invoice_policy_id: ByteArray, + tx: Transaction, +) -> Bool { + // Must be currently listed + expect listing.status == Listed + + // Seller must sign + expect list.has(tx.extra_signatories, listing.seller) + + // NFT must be returned to seller + expect + nft_goes_to_pkh(tx, invoice_policy_id, listing.invoice_id, listing.seller) + + True +} + +// --- Helpers --- + +fn pays_to_pkh( + tx: Transaction, + pkh: ByteArray, + min_lovelace: Int, +) -> Bool { + list.any( + tx.outputs, + fn(o) { + when o.address.payment_credential is { + VerificationKey(hash) -> + hash == pkh && assets.lovelace_of(o.value) >= min_lovelace + _ -> False + } + }, + ) +} + +fn nft_goes_to_pkh( + tx: Transaction, + policy_id: ByteArray, + asset_name: ByteArray, + pkh: ByteArray, +) -> Bool { + list.any( + tx.outputs, + fn(o) { + when o.address.payment_credential is { + VerificationKey(hash) -> + hash == pkh && assets.quantity_of(o.value, policy_id, asset_name) == 1 + _ -> False + } + }, + ) +} + +fn escrow_output_has_buyer( + tx: Transaction, + escrow_script_hash: ByteArray, + invoice_id: ByteArray, + buyer_pkh: ByteArray, +) -> Bool { + list.any( + tx.outputs, + fn(o) { + when o.address.payment_credential is { + Script(hash) -> + if hash == escrow_script_hash { + when o.datum is { + InlineDatum(raw) -> { + expect datum: EscrowDatum = raw + datum.invoice_id == invoice_id && datum.buyer == Some( + buyer_pkh, + ) && datum.status == Locked + } + _ -> False + } + } else { + False + } + _ -> False + } + }, + ) +} diff --git a/lazer/cardano/factura-ya/frontend/index.html b/lazer/cardano/factura-ya/frontend/index.html new file mode 100644 index 00000000..0e648285 --- /dev/null +++ b/lazer/cardano/factura-ya/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + Factura Ya — Invoice Marketplace + + +
+ + + diff --git a/lazer/cardano/factura-ya/frontend/package.json b/lazer/cardano/factura-ya/frontend/package.json new file mode 100644 index 00000000..605f44e0 --- /dev/null +++ b/lazer/cardano/factura-ya/frontend/package.json @@ -0,0 +1,22 @@ +{ + "name": "factura-ya-frontend", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.0", + "typescript": "^5.4.0", + "vite": "^5.4.0" + } +} diff --git a/lazer/cardano/factura-ya/frontend/src/App.tsx b/lazer/cardano/factura-ya/frontend/src/App.tsx new file mode 100644 index 00000000..a137d9d9 --- /dev/null +++ b/lazer/cardano/factura-ya/frontend/src/App.tsx @@ -0,0 +1,55 @@ +import { useState } from "react"; +import { Marketplace } from "./components/Marketplace.tsx"; +import { RegisterInvoice } from "./components/RegisterInvoice.tsx"; +import { PriceDisplay } from "./components/PriceDisplay.tsx"; +import { WalletConnect } from "./components/WalletConnect.tsx"; +import type { ConnectedWallet } from "./lib/wallet.ts"; + +type Tab = "marketplace" | "register"; + +export function App() { + const [tab, setTab] = useState("marketplace"); + const [wallet, setWallet] = useState(null); + + return ( +
+
+
+

Factura Ya

+ setWallet(null)} + /> +
+

+ Sell your invoices, get paid today — powered by Cardano & Pyth +

+ + +
+
+ {tab === "marketplace" && } + {tab === "register" && } +
+
+

+ Built for the Pythathon hackathon by Facturas Ya — Cardano + Pyth + TxPipe +

+
+
+ ); +} diff --git a/lazer/cardano/factura-ya/frontend/src/components/Marketplace.tsx b/lazer/cardano/factura-ya/frontend/src/components/Marketplace.tsx new file mode 100644 index 00000000..ca064cc1 --- /dev/null +++ b/lazer/cardano/factura-ya/frontend/src/components/Marketplace.tsx @@ -0,0 +1,95 @@ +import { useEffect, useState } from "react"; +import { fetchInvoices, type IndexedInvoice } from "../lib/api.ts"; + +function lovelaceToAda(lovelace: number): string { + return (lovelace / 1_000_000).toFixed(2); +} + +function daysUntil(posixMs: number): number { + return Math.ceil((posixMs - Date.now()) / (1000 * 60 * 60 * 24)); +} + +export function Marketplace() { + const [invoices, setInvoices] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetchInvoices() + .then(setInvoices) + .catch(() => setInvoices([])) + .finally(() => setLoading(false)); + + const interval = setInterval(() => { + fetchInvoices().then(setInvoices).catch(() => {}); + }, 10_000); + + return () => clearInterval(interval); + }, []); + + if (loading) return

Loading marketplace...

; + + if (invoices.length === 0) { + return ( +
+

Marketplace

+

No invoices listed yet. Be the first to tokenize!

+
+ ); + } + + return ( +
+

Invoice Marketplace

+

{invoices.length} invoice(s) available

+
+ {invoices.map((inv) => ( + + ))} +
+
+ ); +} + +function InvoiceCard({ invoice }: { invoice: IndexedInvoice }) { + const days = daysUntil(invoice.dueDatePosixMs); + const discount = + invoice.originalAmountArs > 0 + ? ( + ((invoice.originalAmountArs - invoice.askingPriceLovelace) / + invoice.originalAmountArs) * + 100 + ).toFixed(1) + : "0"; + + return ( +
+
+ + {invoice.invoiceId.slice(0, 8)}... + + + {invoice.status} + +
+
+
+ + {invoice.originalAmountArs.toLocaleString()} +
+
+ + {lovelaceToAda(invoice.askingPriceLovelace)} +
+
+ + {discount}% +
+
+ + {days > 0 ? `${days} days` : "Overdue"} +
+
+ +
+ ); +} diff --git a/lazer/cardano/factura-ya/frontend/src/components/PriceDisplay.tsx b/lazer/cardano/factura-ya/frontend/src/components/PriceDisplay.tsx new file mode 100644 index 00000000..5008508d --- /dev/null +++ b/lazer/cardano/factura-ya/frontend/src/components/PriceDisplay.tsx @@ -0,0 +1,28 @@ +import { useEffect, useState } from "react"; + +export function PriceDisplay() { + const [adaPrice, setAdaPrice] = useState(null); + + useEffect(() => { + // In production: connects to PythPriceClient WebSocket + // For MVP: mock or fetch from indexer + const mockPrice = () => { + // Simulate ADA/USD around $0.65-0.75 + setAdaPrice(0.65 + Math.random() * 0.1); + }; + + mockPrice(); + const interval = setInterval(mockPrice, 5000); + return () => clearInterval(interval); + }, []); + + return ( +
+ ADA/USD + + {adaPrice !== null ? `$${adaPrice.toFixed(4)}` : "Loading..."} + + via Pyth +
+ ); +} diff --git a/lazer/cardano/factura-ya/frontend/src/components/RegisterInvoice.tsx b/lazer/cardano/factura-ya/frontend/src/components/RegisterInvoice.tsx new file mode 100644 index 00000000..3c5feadc --- /dev/null +++ b/lazer/cardano/factura-ya/frontend/src/components/RegisterInvoice.tsx @@ -0,0 +1,142 @@ +import { useState, type FormEvent } from "react"; +import type { ConnectedWallet } from "../lib/wallet.ts"; +import { registerInvoice, type TxResult } from "../lib/transactions.ts"; + +interface Props { + wallet: ConnectedWallet | null; +} + +interface InvoiceFormData { + amountArs: string; + dueDateDays: string; + debtorName: string; + debtorContact: string; +} + +export function RegisterInvoice({ wallet }: Props) { + const [form, setForm] = useState({ + amountArs: "", + dueDateDays: "90", + debtorName: "", + debtorContact: "", + }); + const [submitted, setSubmitted] = useState(false); + const [txResult, setTxResult] = useState(null); + const [loading, setLoading] = useState(false); + + if (!wallet) { + return ( +
+

Register Invoice

+

Connect your wallet to register an invoice.

+
+ ); + } + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + setLoading(true); + try { + const result = await registerInvoice({ + amountArs: Number(form.amountArs), + dueDateDays: Number(form.dueDateDays), + debtorName: form.debtorName, + debtorContact: form.debtorContact, + sellerAddress: wallet.address, + }); + setTxResult(result); + setSubmitted(true); + } catch (err) { + setTxResult({ + success: false, + error: err instanceof Error ? err.message : "Transaction failed", + }); + } finally { + setLoading(false); + } + }; + + if (submitted) { + return ( +
+

Invoice Registered!

+

+ Your invoice for {Number(form.amountArs).toLocaleString()} ARS has + been tokenized and listed on the marketplace. +

+ {txResult?.txHash && ( +

+ Tx: {txResult.txHash} +

+ )} +

+ Collateral locked. NFT minted. Listed for investors. +

+ +
+ ); + } + + return ( +
+

Register Invoice

+
+
+ + setForm({ ...form, amountArs: e.target.value })} + required + /> +
+
+ + setForm({ ...form, dueDateDays: e.target.value })} + required + /> +
+
+ + setForm({ ...form, debtorName: e.target.value })} + required + /> +
+
+ + + setForm({ ...form, debtorContact: e.target.value }) + } + required + /> +
+
+

Collateral: ~10% of invoice value will be locked as guarantee

+

Wallet: {wallet.info.name} connected

+
+ + {txResult && !txResult.success && ( +

{txResult.error}

+ )} +
+
+ ); +} diff --git a/lazer/cardano/factura-ya/frontend/src/components/WalletConnect.tsx b/lazer/cardano/factura-ya/frontend/src/components/WalletConnect.tsx new file mode 100644 index 00000000..6de3b65e --- /dev/null +++ b/lazer/cardano/factura-ya/frontend/src/components/WalletConnect.tsx @@ -0,0 +1,90 @@ +import { useState, useEffect } from "react"; +import { + getAvailableWallets, + connectWallet, + shortenAddress, + lovelaceToAda, + type WalletInfo, + type ConnectedWallet, +} from "../lib/wallet.ts"; + +interface Props { + onConnect: (wallet: ConnectedWallet) => void; + onDisconnect: () => void; + connected: ConnectedWallet | null; +} + +export function WalletConnect({ onConnect, onDisconnect, connected }: Props) { + const [wallets, setWallets] = useState([]); + const [connecting, setConnecting] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + // Small delay to let wallet extensions inject + const timer = setTimeout(() => { + setWallets(getAvailableWallets()); + }, 500); + return () => clearTimeout(timer); + }, []); + + const handleConnect = async (walletId: string) => { + setConnecting(true); + setError(null); + try { + const wallet = await connectWallet(walletId); + if (wallet.networkId !== 0) { + setError("Please switch to PreProd testnet"); + return; + } + onConnect(wallet); + } catch (err) { + setError(err instanceof Error ? err.message : "Connection failed"); + } finally { + setConnecting(false); + } + }; + + if (connected) { + return ( +
+
+ + + {shortenAddress(connected.address)} + + + {lovelaceToAda(connected.balanceLovelace)} ADA + +
+ +
+ ); + } + + return ( +
+ {wallets.length === 0 ? ( +

+ No CIP-30 wallet detected. Install Nami, Eternl, or Lace. +

+ ) : ( +
+ {wallets.map((w) => ( + + ))} +
+ )} + {error &&

{error}

} +
+ ); +} diff --git a/lazer/cardano/factura-ya/frontend/src/index.css b/lazer/cardano/factura-ya/frontend/src/index.css new file mode 100644 index 00000000..d46d5126 --- /dev/null +++ b/lazer/cardano/factura-ya/frontend/src/index.css @@ -0,0 +1,149 @@ +* { box-sizing: border-box; margin: 0; padding: 0; } + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + background: #0a0a0f; + color: #e0e0e0; + min-height: 100vh; +} + +.app { max-width: 960px; margin: 0 auto; padding: 1rem; } + +header { + padding: 2rem 0 1rem; + border-bottom: 1px solid #222; + margin-bottom: 2rem; + text-align: center; +} + +.header-top { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; +} + +h1 { font-size: 2rem; color: #4fc3f7; } +h2 { font-size: 1.4rem; margin-bottom: 1rem; color: #b0bec5; } +.subtitle { color: #666; margin: 0.5rem 0 1rem; } + +nav { display: flex; gap: 0.5rem; justify-content: center; margin-top: 1rem; } +nav button { + padding: 0.5rem 1.5rem; + border: 1px solid #333; + background: transparent; + color: #999; + border-radius: 6px; + cursor: pointer; + font-size: 0.9rem; +} +nav button.active { background: #1a237e; color: #4fc3f7; border-color: #4fc3f7; } + +.price-display { + display: inline-flex; + align-items: center; + gap: 0.5rem; + background: #111; + padding: 0.4rem 1rem; + border-radius: 20px; + font-size: 0.85rem; +} +.price-label { color: #666; } +.price-value { color: #4caf50; font-weight: bold; } +.price-source { color: #555; font-size: 0.75rem; } + +.invoice-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1rem; + margin-top: 1rem; +} + +.invoice-card { + background: #111; + border: 1px solid #222; + border-radius: 10px; + padding: 1rem; +} +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; +} +.invoice-id { font-family: monospace; color: #888; font-size: 0.8rem; } +.status { font-size: 0.75rem; padding: 2px 8px; border-radius: 10px; } +.status.listed { background: #1b5e20; color: #81c784; } +.status.sold { background: #4a148c; color: #ce93d8; } + +.card-body { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; } +.field label { display: block; font-size: 0.7rem; color: #555; text-transform: uppercase; } +.field span { font-size: 1rem; } + +.buy-btn { + width: 100%; + margin-top: 1rem; + padding: 0.6rem; + background: #1a237e; + color: #4fc3f7; + border: 1px solid #4fc3f7; + border-radius: 6px; + cursor: pointer; + font-weight: bold; +} +.buy-btn:hover { background: #283593; } + +.invoice-form { max-width: 400px; } +.form-field { margin-bottom: 1rem; } +.form-field label { display: block; margin-bottom: 0.3rem; color: #888; font-size: 0.85rem; } +.form-field input { + width: 100%; + padding: 0.6rem; + background: #111; + border: 1px solid #333; + border-radius: 6px; + color: #e0e0e0; + font-size: 0.95rem; +} +.form-field input:focus { border-color: #4fc3f7; outline: none; } + +.submit-btn { + padding: 0.7rem 2rem; + background: #4fc3f7; + color: #0a0a0f; + border: none; + border-radius: 6px; + cursor: pointer; + font-weight: bold; + font-size: 1rem; +} + +.success { text-align: center; padding: 2rem; } +.success h3 { color: #4caf50; margin-bottom: 0.5rem; } +.success button { margin-top: 1rem; padding: 0.5rem 1.5rem; background: #222; color: #999; border: 1px solid #333; border-radius: 6px; cursor: pointer; } +.empty { text-align: center; padding: 2rem; color: #666; } + +footer { text-align: center; padding: 2rem 0; color: #444; font-size: 0.8rem; border-top: 1px solid #222; margin-top: 2rem; } + +/* Wallet */ +.wallet-connect { display: flex; align-items: center; gap: 0.5rem; } +.wallet-list { display: flex; gap: 0.5rem; } +.wallet-btn { + display: flex; align-items: center; gap: 0.4rem; + padding: 0.4rem 0.8rem; background: #1a237e; color: #4fc3f7; + border: 1px solid #4fc3f7; border-radius: 6px; cursor: pointer; + font-size: 0.8rem; +} +.wallet-btn:hover { background: #283593; } +.wallet-btn:disabled { opacity: 0.5; cursor: wait; } +.wallet-icon { width: 20px; height: 20px; border-radius: 4px; } +.wallet-connected { display: flex; align-items: center; gap: 0.75rem; } +.wallet-info { display: flex; align-items: center; gap: 0.5rem; background: #111; padding: 0.3rem 0.8rem; border-radius: 20px; font-size: 0.8rem; } +.wallet-address { font-family: monospace; color: #888; } +.wallet-balance { color: #4caf50; font-weight: bold; } +.disconnect-btn { padding: 0.3rem 0.6rem; background: transparent; color: #666; border: 1px solid #333; border-radius: 6px; cursor: pointer; font-size: 0.75rem; } +.no-wallet { color: #666; font-size: 0.8rem; } +.wallet-error { color: #ef5350; font-size: 0.8rem; margin-top: 0.3rem; } +.form-info { background: #111; padding: 0.75rem; border-radius: 6px; margin-bottom: 1rem; font-size: 0.85rem; color: #888; } +.form-info p { margin-bottom: 0.25rem; } +.tx-info { color: #666; font-size: 0.9rem; margin-top: 0.5rem; } diff --git a/lazer/cardano/factura-ya/frontend/src/lib/api.ts b/lazer/cardano/factura-ya/frontend/src/lib/api.ts new file mode 100644 index 00000000..8a29d571 --- /dev/null +++ b/lazer/cardano/factura-ya/frontend/src/lib/api.ts @@ -0,0 +1,29 @@ +/** Client for the Factura Ya indexer API. */ + +const API_BASE = "/api"; + +export interface IndexedInvoice { + invoiceId: string; + seller: string; + askingPriceLovelace: number; + originalAmountArs: number; + pythPriceAtListing: number; + pythExponentAtListing: number; + dueDatePosixMs: number; + status: "Listed" | "Sold" | "Delisted"; + buyer: string | null; + txHash: string; + updatedAt: number; +} + +export async function fetchInvoices(): Promise { + const res = await fetch(`${API_BASE}/invoices`); + const data = await res.json(); + return data.invoices; +} + +export async function fetchInvoice(id: string): Promise { + const res = await fetch(`${API_BASE}/invoices/${id}`); + if (!res.ok) throw new Error("Invoice not found"); + return res.json(); +} diff --git a/lazer/cardano/factura-ya/frontend/src/lib/transactions.ts b/lazer/cardano/factura-ya/frontend/src/lib/transactions.ts new file mode 100644 index 00000000..526269a4 --- /dev/null +++ b/lazer/cardano/factura-ya/frontend/src/lib/transactions.ts @@ -0,0 +1,122 @@ +/** + * Transaction construction layer. + * + * Bridges the frontend forms with the off-chain tx builders. + * In the MVP this is a placeholder that logs the tx params — + * full integration requires Evolution SDK + the deployed validator addresses. + */ + +// --- Types --- + +export interface InvoiceRegistration { + amountArs: number; + dueDateDays: number; + debtorName: string; + debtorContact: string; + sellerAddress: string; +} + +export interface TxResult { + success: boolean; + txHash?: string; + error?: string; +} + +// --- Config --- + +export const PYTH_PREPROD_POLICY_ID = + "d799d287105dea9377cdf9ea8502a83d2b9eb2d2050a8aea800a21e6"; + +export const ADA_USD_FEED_ID = 16; + +// --- Placeholder tx builders --- + +/** + * Register an invoice: mint NFT + lock collateral + list on marketplace. + * + * In a full implementation this would: + * 1. Connect to Pyth Pro WebSocket for current ADA/USD price + * 2. Build mint tx with invoice datum + * 3. Build escrow lock tx with 10% collateral + * 4. Build marketplace list tx + * 5. Attach Pyth price update via withdraw-script + * 6. Sign with CIP-30 wallet + * 7. Submit to Cardano PreProd + */ +export async function registerInvoice( + params: InvoiceRegistration, +): Promise { + console.log("[tx] Registering invoice:", params); + + // Simulate tx construction delay + await new Promise((r) => setTimeout(r, 1500)); + + const invoiceId = generateInvoiceId(params); + const contactHash = hashContact(params.debtorContact); + const dueDate = Date.now() + params.dueDateDays * 24 * 60 * 60 * 1000; + + console.log("[tx] Invoice ID:", invoiceId); + console.log("[tx] Contact hash:", contactHash); + console.log("[tx] Due date:", new Date(dueDate).toISOString()); + console.log("[tx] Would build: MintInvoice + LockEscrow + ListOnMarketplace"); + console.log("[tx] Pyth policy:", PYTH_PREPROD_POLICY_ID); + + // Return mock tx hash (in production: real submitted tx hash) + return { + success: true, + txHash: `mock_${invoiceId.slice(0, 16)}`, + }; +} + +/** + * Purchase an invoice from the marketplace. + */ +export async function purchaseInvoice( + invoiceId: string, + buyerAddress: string, +): Promise { + console.log("[tx] Purchasing invoice:", invoiceId, "buyer:", buyerAddress); + await new Promise((r) => setTimeout(r, 1500)); + + return { + success: true, + txHash: `mock_purchase_${invoiceId.slice(0, 8)}`, + }; +} + +/** + * Confirm settlement (release collateral to seller). + */ +export async function confirmSettlement( + invoiceId: string, +): Promise { + console.log("[tx] Confirming settlement for:", invoiceId); + await new Promise((r) => setTimeout(r, 1500)); + + return { + success: true, + txHash: `mock_settle_${invoiceId.slice(0, 8)}`, + }; +} + +// --- Helpers --- + +function generateInvoiceId(params: InvoiceRegistration): string { + const raw = `${params.sellerAddress}-${params.amountArs}-${Date.now()}`; + return toHex(raw).slice(0, 32); +} + +function hashContact(contact: string): string { + // Simple hash for MVP (in production: use SHA-256) + let hash = 0; + for (let i = 0; i < contact.length; i++) { + hash = ((hash << 5) - hash + contact.charCodeAt(i)) | 0; + } + return Math.abs(hash).toString(16).padStart(16, "0"); +} + +function toHex(str: string): string { + return Array.from(new TextEncoder().encode(str)) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); +} diff --git a/lazer/cardano/factura-ya/frontend/src/lib/wallet.ts b/lazer/cardano/factura-ya/frontend/src/lib/wallet.ts new file mode 100644 index 00000000..2f468fd7 --- /dev/null +++ b/lazer/cardano/factura-ya/frontend/src/lib/wallet.ts @@ -0,0 +1,136 @@ +/** + * CIP-30 wallet connection for Cardano dApps. + * + * Supports Nami, Eternl, Flint, Lace, and any CIP-30 compliant wallet. + */ + +// CIP-30 types +interface CardanoWalletApi { + getNetworkId(): Promise; + getUsedAddresses(): Promise; + getUnusedAddresses(): Promise; + getBalance(): Promise; + getUtxos(): Promise; + signTx(tx: string, partialSign?: boolean): Promise; + submitTx(tx: string): Promise; +} + +interface CardanoWallet { + name: string; + icon: string; + apiVersion: string; + enable(): Promise; + isEnabled(): Promise; +} + +declare global { + interface Window { + cardano?: Record; + } +} + +export interface WalletInfo { + name: string; + icon: string; + id: string; +} + +export interface ConnectedWallet { + info: WalletInfo; + api: CardanoWalletApi; + address: string; + networkId: number; + balanceLovelace: bigint; +} + +/** Detect available CIP-30 wallets. */ +export function getAvailableWallets(): WalletInfo[] { + if (!window.cardano) return []; + + const known = ["nami", "eternl", "flint", "lace", "yoroi", "typhon", "gerowallet"]; + const wallets: WalletInfo[] = []; + + for (const id of known) { + const w = window.cardano[id]; + if (w && w.name && typeof w.enable === "function") { + wallets.push({ name: w.name, icon: w.icon, id }); + } + } + + return wallets; +} + +/** Connect to a CIP-30 wallet by ID. */ +export async function connectWallet(walletId: string): Promise { + const wallet = window.cardano?.[walletId]; + if (!wallet) throw new Error(`Wallet ${walletId} not found`); + + const api = await wallet.enable(); + const networkId = await api.getNetworkId(); + const addresses = await api.getUsedAddresses(); + const address = addresses[0] ?? (await api.getUnusedAddresses())[0] ?? ""; + const balanceHex = await api.getBalance(); + // Balance is CBOR-encoded; for simplicity parse the lovelace from hex + const balanceLovelace = parseBalanceHex(balanceHex); + + return { + info: { name: wallet.name, icon: wallet.icon, id: walletId }, + api, + address, + networkId, + balanceLovelace, + }; +} + +/** Parse CBOR balance to lovelace (simplified — handles basic integer encoding). */ +function parseBalanceHex(hex: string): bigint { + try { + // CBOR integers: if first byte < 0x18, value is the byte itself + // 0x18 = 1-byte uint follows, 0x19 = 2-byte, 0x1a = 4-byte, 0x1b = 8-byte + const bytes = hexToBytes(hex); + if (bytes.length === 0) return 0n; + + const first = bytes[0]; + if (first <= 0x17) return BigInt(first); + if (first === 0x18 && bytes.length >= 2) return BigInt(bytes[1]); + if (first === 0x19 && bytes.length >= 3) { + return BigInt((bytes[1] << 8) | bytes[2]); + } + if (first === 0x1a && bytes.length >= 5) { + return BigInt( + (bytes[1] << 24) | (bytes[2] << 16) | (bytes[3] << 8) | bytes[4], + ); + } + if (first === 0x1b && bytes.length >= 9) { + let val = 0n; + for (let i = 1; i <= 8; i++) { + val = (val << 8n) | BigInt(bytes[i]); + } + return val; + } + // Fallback for complex CBOR (maps with multi-asset) + return 0n; + } catch { + return 0n; + } +} + +function hexToBytes(hex: string): Uint8Array { + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < hex.length; i += 2) { + bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16); + } + return bytes; +} + +/** Shorten an address for display. */ +export function shortenAddress(addr: string): string { + if (addr.length <= 20) return addr; + return `${addr.slice(0, 10)}...${addr.slice(-8)}`; +} + +/** Format lovelace as ADA. */ +export function lovelaceToAda(lovelace: bigint): string { + const ada = Number(lovelace) / 1_000_000; + return ada.toFixed(2); +} diff --git a/lazer/cardano/factura-ya/frontend/src/main.tsx b/lazer/cardano/factura-ya/frontend/src/main.tsx new file mode 100644 index 00000000..71675c47 --- /dev/null +++ b/lazer/cardano/factura-ya/frontend/src/main.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import { App } from "./App.tsx"; +import "./index.css"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/lazer/cardano/factura-ya/frontend/tsconfig.json b/lazer/cardano/factura-ya/frontend/tsconfig.json new file mode 100644 index 00000000..9387615b --- /dev/null +++ b/lazer/cardano/factura-ya/frontend/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false + }, + "include": ["src"] +} diff --git a/lazer/cardano/factura-ya/frontend/vite.config.ts b/lazer/cardano/factura-ya/frontend/vite.config.ts new file mode 100644 index 00000000..3686c956 --- /dev/null +++ b/lazer/cardano/factura-ya/frontend/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + server: { + proxy: { + "/api": { + target: "http://localhost:3001", + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, ""), + }, + }, + }, +}); diff --git a/lazer/cardano/factura-ya/indexer/oura.toml b/lazer/cardano/factura-ya/indexer/oura.toml new file mode 100644 index 00000000..b39252e4 --- /dev/null +++ b/lazer/cardano/factura-ya/indexer/oura.toml @@ -0,0 +1,31 @@ +# Oura pipeline configuration for Factura Ya marketplace indexer. +# +# Watches the marketplace validator script address on Cardano PreProd +# and pipes transaction events to a webhook sink for processing. +# +# Prerequisites: +# - Oura installed (https://txpipe.github.io/oura/) +# - Cardano PreProd node access (via Demeter.run or local) +# - The indexer API running on localhost:3001 + +[source] +type = "N2N" +peers = ["preprod-node.world.dev.cardano.org:30000"] + +[source.mapper] +include_transaction_details = true + +# Filter for marketplace validator script address +# Replace MARKETPLACE_SCRIPT_ADDRESS with the actual bech32 address +# after deploying the marketplace validator. +[[filters]] +type = "selection" + +[filters.predicate] +any_output_address = "MARKETPLACE_SCRIPT_ADDRESS" + +[sink] +type = "Webhook" +url = "http://localhost:3001/oura/event" +timeout = 30000 +max_retries = 3 diff --git a/lazer/cardano/factura-ya/indexer/package.json b/lazer/cardano/factura-ya/indexer/package.json new file mode 100644 index 00000000..a1bf8c04 --- /dev/null +++ b/lazer/cardano/factura-ya/indexer/package.json @@ -0,0 +1,20 @@ +{ + "name": "factura-ya-indexer", + "version": "0.0.1", + "description": "Oura-powered marketplace indexer for Factura Ya", + "type": "module", + "scripts": { + "start": "tsx src/api.ts", + "build": "tsc", + "check": "tsc --noEmit" + }, + "dependencies": { + "express": "^4.21.0" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^20.0.0", + "tsx": "^4.7.0", + "typescript": "^5.4.0" + } +} diff --git a/lazer/cardano/factura-ya/indexer/src/api.ts b/lazer/cardano/factura-ya/indexer/src/api.ts new file mode 100644 index 00000000..fdad217c --- /dev/null +++ b/lazer/cardano/factura-ya/indexer/src/api.ts @@ -0,0 +1,176 @@ +/** + * Simple REST API + Oura webhook receiver for the Factura Ya marketplace indexer. + * + * Stores marketplace state in-memory (JSON). For a hackathon MVP this is + * sufficient — a production version would use PostgreSQL. + * + * Endpoints: + * GET /invoices — all active listings + * GET /invoices/:id — single invoice detail + * POST /oura/event — webhook sink for Oura pipeline events + */ + +import express from "express"; + +const app = express(); +app.use(express.json({ limit: "1mb" })); + +const PORT = process.env.PORT ?? 3001; + +// --- In-memory store --- + +interface IndexedInvoice { + invoiceId: string; + seller: string; + askingPriceLovelace: number; + originalAmountArs: number; + pythPriceAtListing: number; + pythExponentAtListing: number; + dueDatePosixMs: number; + status: "Listed" | "Sold" | "Delisted"; + buyer: string | null; + txHash: string; + updatedAt: number; +} + +const invoices = new Map(); + +// --- API routes --- + +app.get("/invoices", (_req, res) => { + const active = [...invoices.values()].filter((i) => i.status === "Listed"); + res.json({ invoices: active, total: active.length }); +}); + +app.get("/invoices/:id", (req, res) => { + const invoice = invoices.get(req.params.id); + if (!invoice) { + res.status(404).json({ error: "Invoice not found" }); + return; + } + res.json(invoice); +}); + +// Return all invoices regardless of status (for debugging / frontend) +app.get("/invoices/all", (_req, res) => { + res.json({ invoices: [...invoices.values()], total: invoices.size }); +}); + +// --- Oura webhook receiver --- + +/** + * Receives transaction events from Oura. + * + * Oura sends events as JSON with transaction details. We parse the + * marketplace datum from outputs to index listings. + * + * Event shape (simplified): + * { + * context: { tx_hash, block_number, slot, timestamp }, + * transaction: { + * outputs: [{ address, datum: { fields: [...] }, ... }] + * } + * } + */ +app.post("/oura/event", (req, res) => { + try { + const event = req.body; + processOuraEvent(event); + res.status(200).json({ ok: true }); + } catch (err) { + console.error("Error processing Oura event:", err); + res.status(500).json({ error: "Failed to process event" }); + } +}); + +function processOuraEvent(event: Record): void { + const ctx = event.context as { tx_hash?: string; timestamp?: number } | undefined; + const tx = event.transaction as { + outputs?: Array<{ + address?: string; + datum?: { fields?: unknown[] }; + }>; + } | undefined; + + if (!ctx?.tx_hash || !tx?.outputs) return; + + for (const output of tx.outputs) { + if (!output.datum?.fields) continue; + tryParseListingDatum(output.datum.fields, ctx.tx_hash, ctx.timestamp ?? Date.now()); + } +} + +/** + * Attempt to parse a ListingDatum from Oura datum fields. + * + * Expected field order matches the on-chain ListingDatum: + * [invoice_id, seller, asking_price_lovelace, original_amount_ars, + * pyth_price_at_listing, pyth_exponent_at_listing, due_date_posix_ms, status] + */ +function tryParseListingDatum( + fields: unknown[], + txHash: string, + timestamp: number, +): void { + if (fields.length < 8) return; + + try { + const invoiceId = String(fields[0]); + const seller = String(fields[1]); + const askingPriceLovelace = Number(fields[2]); + const originalAmountArs = Number(fields[3]); + const pythPriceAtListing = Number(fields[4]); + const pythExponentAtListing = Number(fields[5]); + const dueDatePosixMs = Number(fields[6]); + const statusRaw = fields[7]; + + let status: "Listed" | "Sold" | "Delisted" = "Listed"; + if (typeof statusRaw === "object" && statusRaw !== null) { + const tag = (statusRaw as { constructor?: number }).constructor; + if (tag === 0) status = "Listed"; + else if (tag === 1) status = "Sold"; + else if (tag === 2) status = "Delisted"; + } + + invoices.set(invoiceId, { + invoiceId, + seller, + askingPriceLovelace, + originalAmountArs, + pythPriceAtListing, + pythExponentAtListing, + dueDatePosixMs, + status, + buyer: null, + txHash, + updatedAt: timestamp, + }); + + console.log(`[indexer] ${status} invoice ${invoiceId} (tx: ${txHash.slice(0, 8)}...)`); + } catch { + // Not a listing datum, skip + } +} + +// --- Manual seeding (for demo) --- + +app.post("/invoices/seed", (req, res) => { + const invoice = req.body as IndexedInvoice; + if (!invoice.invoiceId) { + res.status(400).json({ error: "Missing invoiceId" }); + return; + } + invoices.set(invoice.invoiceId, { + ...invoice, + updatedAt: Date.now(), + }); + res.status(201).json({ ok: true, invoiceId: invoice.invoiceId }); +}); + +// --- Start --- + +app.listen(PORT, () => { + console.log(`[indexer] Factura Ya indexer API running on port ${PORT}`); +}); + +export { app, invoices }; diff --git a/lazer/cardano/factura-ya/indexer/tsconfig.json b/lazer/cardano/factura-ya/indexer/tsconfig.json new file mode 100644 index 00000000..ff8c73b6 --- /dev/null +++ b/lazer/cardano/factura-ya/indexer/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true + }, + "include": ["src/**/*"] +} diff --git a/lazer/cardano/factura-ya/offchain/package.json b/lazer/cardano/factura-ya/offchain/package.json new file mode 100644 index 00000000..4c5e9b1f --- /dev/null +++ b/lazer/cardano/factura-ya/offchain/package.json @@ -0,0 +1,18 @@ +{ + "name": "factura-ya-offchain", + "version": "0.0.1", + "description": "Off-chain transaction builders for Factura Ya", + "type": "module", + "scripts": { + "build": "tsc", + "check": "tsc --noEmit" + }, + "dependencies": { + "@pythnetwork/pyth-lazer-cardano-js": "^0.1.0", + "@pythnetwork/pyth-lazer-sdk": "^0.3.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.4.0" + } +} diff --git a/lazer/cardano/factura-ya/offchain/src/escrow.ts b/lazer/cardano/factura-ya/offchain/src/escrow.ts new file mode 100644 index 00000000..167f6f3e --- /dev/null +++ b/lazer/cardano/factura-ya/offchain/src/escrow.ts @@ -0,0 +1,139 @@ +/** + * Off-chain transaction builders for the escrow / collateral validator. + * + * Three operations: + * - Lock: create escrow UTxO with collateral (typically in same tx as mint) + * - Release: buyer confirms payment, collateral returned to seller + * - Forfeit: buyer reports non-payment, collateral transferred to buyer + */ + +// --- Types --- + +export interface EscrowDatumFields { + invoice_id: string; + seller: string; + collateral_lovelace: number; + buyer: string | null; + due_date_posix_ms: number; + status: "Locked" | "Released" | "Forfeited"; +} + +export interface LockEscrowParams { + datum: EscrowDatumFields; + /** Lovelace to lock as collateral. */ + collateralLovelace: number; + /** The escrow validator script address. */ + escrowAddress: string; +} + +export interface ReleaseEscrowParams { + /** The UTxO reference of the escrow to consume. */ + escrowUtxoRef: string; + /** The seller's address to send collateral to. */ + sellerAddress: string; + /** Lovelace amount to return. */ + collateralLovelace: number; + redeemer: "Release"; +} + +export interface ForfeitEscrowParams { + /** The UTxO reference of the escrow to consume. */ + escrowUtxoRef: string; + /** The buyer's address to send collateral to. */ + buyerAddress: string; + /** Lovelace amount to transfer. */ + collateralLovelace: number; + redeemer: "Forfeit"; +} + +// --- Constants --- + +/** Grace period after due date before forfeit is allowed (7 days in ms). */ +export const GRACE_PERIOD_MS = 7 * 24 * 60 * 60 * 1000; + +/** Minimum collateral percentage in basis points. 1000 = 10%. */ +export const MIN_COLLATERAL_BPS = 1000; + +// --- Builders --- + +/** + * Calculate the minimum collateral required for an invoice. + * + * @param invoiceAmountLovelace - The invoice value converted to lovelace. + * @param collateralBps - Collateral percentage in basis points (default: 1000 = 10%). + */ +export function calculateCollateral( + invoiceAmountLovelace: number, + collateralBps: number = MIN_COLLATERAL_BPS, +): number { + return Math.ceil((invoiceAmountLovelace * collateralBps) / 10_000); +} + +/** + * Build parameters for creating an escrow UTxO (lock collateral). + * + * Typically called alongside buildMintInvoiceParams so the escrow + * is created in the same transaction as the NFT mint. + */ +export function buildLockEscrowParams( + invoiceId: string, + sellerPkh: string, + collateralLovelace: number, + dueDatePosixMs: number, + escrowAddress: string, +): LockEscrowParams { + return { + datum: { + invoice_id: invoiceId, + seller: sellerPkh, + collateral_lovelace: collateralLovelace, + buyer: null, + due_date_posix_ms: dueDatePosixMs, + status: "Locked", + }, + collateralLovelace, + escrowAddress, + }; +} + +/** + * Build parameters for releasing collateral back to the seller. + * + * The caller must: + * 1. Include the buyer's signature. + * 2. Set validity range lower bound >= due_date_posix_ms. + * 3. Include an output paying collateral to the seller. + */ +export function buildReleaseEscrowParams( + escrowUtxoRef: string, + sellerAddress: string, + collateralLovelace: number, +): ReleaseEscrowParams { + return { + escrowUtxoRef, + sellerAddress, + collateralLovelace, + redeemer: "Release", + }; +} + +/** + * Build parameters for forfeiting collateral to the buyer. + * + * The caller must: + * 1. Include the buyer's signature. + * 2. Set validity range lower bound >= due_date_posix_ms + GRACE_PERIOD_MS. + * 3. Include an output paying collateral to the buyer. + */ +export function buildForfeitEscrowParams( + escrowUtxoRef: string, + buyerAddress: string, + collateralLovelace: number, +): ForfeitEscrowParams { + return { + escrowUtxoRef, + buyerAddress, + collateralLovelace, + redeemer: "Forfeit", + }; +} diff --git a/lazer/cardano/factura-ya/offchain/src/marketplace.ts b/lazer/cardano/factura-ya/offchain/src/marketplace.ts new file mode 100644 index 00000000..fd01e79e --- /dev/null +++ b/lazer/cardano/factura-ya/offchain/src/marketplace.ts @@ -0,0 +1,170 @@ +/** + * Off-chain transaction builders for the marketplace validator. + * + * Three operations: + * - List: SME sends invoice NFT to marketplace, creates listing UTxO + * - Purchase: investor buys listed invoice, payment to seller, NFT to buyer + * - Delist: seller withdraws listing, NFT returned + */ + +// --- Types --- + +export interface ListingDatumFields { + invoice_id: string; + seller: string; + asking_price_lovelace: number; + original_amount_ars: number; + pyth_price_at_listing: number; + pyth_exponent_at_listing: number; + due_date_posix_ms: number; + status: "Listed" | "Sold" | "Delisted"; +} + +export interface ListInvoiceParams { + /** Datum for the listing UTxO. */ + datum: ListingDatumFields; + /** The invoice NFT asset name. */ + assetName: string; + /** The invoice minting policy ID. */ + invoicePolicyId: string; + /** The marketplace validator address. */ + marketplaceAddress: string; +} + +export interface PurchaseInvoiceParams { + /** The listing UTxO reference to consume. */ + listingUtxoRef: string; + /** Lovelace to pay to the seller. */ + paymentLovelace: number; + /** Seller's address (for payment output). */ + sellerAddress: string; + /** Buyer's PKH (for NFT output and escrow update). */ + buyerPkh: string; + /** The invoice NFT asset name. */ + assetName: string; + /** The invoice minting policy ID. */ + invoicePolicyId: string; + /** The escrow UTxO to consume and recreate with buyer set. */ + escrowUtxoRef: string; + /** Updated escrow datum with buyer field set. */ + updatedEscrowDatum: { + invoice_id: string; + seller: string; + collateral_lovelace: number; + buyer: string; + due_date_posix_ms: number; + status: "Locked"; + }; + redeemer: "Purchase"; +} + +export interface DelistInvoiceParams { + /** The listing UTxO reference to consume. */ + listingUtxoRef: string; + /** The seller's address (to return NFT). */ + sellerAddress: string; + /** The invoice NFT asset name. */ + assetName: string; + /** The invoice minting policy ID. */ + invoicePolicyId: string; + redeemer: "Delist"; +} + +// --- Builders --- + +/** + * Build parameters for listing an invoice on the marketplace. + * + * The caller must also attach the Pyth price update in the same tx + * so the listing price can be validated. + */ +export function buildListInvoiceParams( + invoiceId: string, + assetName: string, + invoicePolicyId: string, + sellerPkh: string, + askingPriceLovelace: number, + originalAmountArs: number, + pythPrice: number, + pythExponent: number, + dueDatePosixMs: number, + marketplaceAddress: string, +): ListInvoiceParams { + return { + datum: { + invoice_id: invoiceId, + seller: sellerPkh, + asking_price_lovelace: askingPriceLovelace, + original_amount_ars: originalAmountArs, + pyth_price_at_listing: pythPrice, + pyth_exponent_at_listing: pythExponent, + due_date_posix_ms: dueDatePosixMs, + status: "Listed", + }, + assetName, + invoicePolicyId, + marketplaceAddress, + }; +} + +/** + * Build parameters for purchasing a listed invoice. + * + * The caller must: + * 1. Include the buyer's signature. + * 2. Create an output paying >= asking_price_lovelace to the seller. + * 3. Create an output sending the NFT to the buyer. + * 4. Consume the escrow UTxO and recreate it with buyer set. + */ +export function buildPurchaseInvoiceParams( + listingUtxoRef: string, + sellerAddress: string, + buyerPkh: string, + paymentLovelace: number, + assetName: string, + invoicePolicyId: string, + escrowUtxoRef: string, + escrowInvoiceId: string, + escrowSeller: string, + escrowCollateralLovelace: number, + escrowDueDatePosixMs: number, +): PurchaseInvoiceParams { + return { + listingUtxoRef, + paymentLovelace, + sellerAddress, + buyerPkh, + assetName, + invoicePolicyId, + escrowUtxoRef, + updatedEscrowDatum: { + invoice_id: escrowInvoiceId, + seller: escrowSeller, + collateral_lovelace: escrowCollateralLovelace, + buyer: buyerPkh, + due_date_posix_ms: escrowDueDatePosixMs, + status: "Locked", + }, + redeemer: "Purchase", + }; +} + +/** + * Build parameters for delisting an invoice (seller withdraws). + * + * The caller must include the seller's signature. + */ +export function buildDelistInvoiceParams( + listingUtxoRef: string, + sellerAddress: string, + assetName: string, + invoicePolicyId: string, +): DelistInvoiceParams { + return { + listingUtxoRef, + sellerAddress, + assetName, + invoicePolicyId, + redeemer: "Delist", + }; +} diff --git a/lazer/cardano/factura-ya/offchain/src/mint.ts b/lazer/cardano/factura-ya/offchain/src/mint.ts new file mode 100644 index 00000000..1b36c8df --- /dev/null +++ b/lazer/cardano/factura-ya/offchain/src/mint.ts @@ -0,0 +1,117 @@ +/** + * Off-chain transaction builders for minting and burning invoice NFTs. + * + * These are framework-agnostic helpers that produce the transaction + * parameters. The actual tx construction depends on the wallet SDK + * (lucid, mesh, or cardano-serialization-lib). + */ + +import { createHash } from "node:crypto"; + +// --- Types --- + +export interface InvoiceInput { + /** Unique identifier for the invoice. */ + invoiceId: string; + /** Invoice amount in ARS (integer, e.g. cents or smallest unit). */ + amountArs: number; + /** Due date as POSIX milliseconds. */ + dueDatePosixMs: number; + /** Debtor company/person name. */ + debtorName: string; + /** Debtor contact info (email or phone) — will be hashed before on-chain storage. */ + debtorContact: string; +} + +export interface InvoiceDatumFields { + invoice_id: string; + seller: string; + amount_ars: number; + due_date_posix_ms: number; + debtor_name: string; + debtor_contact_hash: string; + created_at_posix_ms: number; +} + +export interface MintInvoiceParams { + /** The invoice NFT asset name (= invoice_id as hex). */ + assetName: string; + /** The InvoiceDatum to attach to the output. */ + datum: InvoiceDatumFields; + /** The redeemer for the minting policy. */ + redeemer: "MintInvoice"; +} + +export interface BurnInvoiceParams { + /** The invoice NFT asset name to burn. */ + assetName: string; + /** The redeemer for the minting policy. */ + redeemer: "BurnInvoice"; +} + +// --- Helpers --- + +/** + * Hash debtor contact info with SHA-256 for on-chain privacy. + * Returns hex-encoded hash. + */ +export function hashDebtorContact(contact: string): string { + return createHash("sha256").update(contact).digest("hex"); +} + +/** + * Convert a UTF-8 string to a hex-encoded ByteArray for use as asset name. + */ +export function toHex(str: string): string { + return Buffer.from(str, "utf-8").toString("hex"); +} + +// --- Builders --- + +/** + * Build parameters for minting a new invoice NFT. + * + * The caller must: + * 1. Include the seller's signature in the transaction. + * 2. Set the validity range with a lower bound (for due date validation). + * 3. Create an output at the desired address holding the NFT + InvoiceDatum. + * 4. Optionally create the escrow UTxO in the same or a follow-up transaction. + */ +export function buildMintInvoiceParams( + invoice: InvoiceInput, + sellerPkh: string, +): MintInvoiceParams { + const assetName = toHex(invoice.invoiceId); + const contactHash = hashDebtorContact(invoice.debtorContact); + + return { + assetName, + datum: { + invoice_id: assetName, + seller: sellerPkh, + amount_ars: invoice.amountArs, + due_date_posix_ms: invoice.dueDatePosixMs, + debtor_name: toHex(invoice.debtorName), + debtor_contact_hash: contactHash, + created_at_posix_ms: Date.now(), + }, + redeemer: "MintInvoice", + }; +} + +/** + * Build parameters for burning an invoice NFT after settlement. + * + * The caller must: + * 1. Include the NFT holder's signature. + * 2. Include the escrow UTxO as a reference input (so the minting policy + * can verify the escrow status is Released or Forfeited). + */ +export function buildBurnInvoiceParams( + invoiceId: string, +): BurnInvoiceParams { + return { + assetName: toHex(invoiceId), + redeemer: "BurnInvoice", + }; +} diff --git a/lazer/cardano/factura-ya/offchain/src/pyth.ts b/lazer/cardano/factura-ya/offchain/src/pyth.ts new file mode 100644 index 00000000..9cfda8d7 --- /dev/null +++ b/lazer/cardano/factura-ya/offchain/src/pyth.ts @@ -0,0 +1,182 @@ +/** + * Pyth Pro integration for Factura Ya. + * + * Uses the official SDKs: + * - @pythnetwork/pyth-lazer-sdk: WebSocket price feed subscription + * - @pythnetwork/pyth-lazer-cardano-js: Cardano transaction helpers + * (getPythState, getPythScriptHash, createEvolutionClient) + */ + +import { + PythLazerClient, + type JsonOrBinaryResponse, +} from "@pythnetwork/pyth-lazer-sdk"; +import { + getPythState, + getPythScriptHash, +} from "@pythnetwork/pyth-lazer-cardano-js"; +import type { UTxO } from "@evolution-sdk/evolution"; +import type { ProviderOnlyClient } from "@evolution-sdk/evolution/sdk/client/Client"; + +// Re-export official helpers for convenience +export { getPythState, getPythScriptHash }; + +// --- Configuration --- + +/** Pyth PreProd deployment Policy ID. */ +export const PYTH_PREPROD_POLICY_ID = + "d799d287105dea9377cdf9ea8502a83d2b9eb2d2050a8aea800a21e6"; + +/** ADA/USD feed ID on Pyth Pro. */ +export const ADA_USD_FEED_ID = 16; + +/** USD/ARS feed ID (coming soon on Pyth Pro). */ +export const USD_ARS_FEED_ID = 2582; + +/** Default WebSocket endpoint for Pyth Pro. */ +const DEFAULT_WS_URL = "wss://pyth-lazer.dourolabs.app/v2/ws"; + +// --- Types --- + +export interface PythClientConfig { + accessToken: string; + wsUrl?: string; + feedIds?: number[]; + numConnections?: number; +} + +// --- Client --- + +/** + * Manages a connection to Pyth Pro WebSocket and caches the latest + * price update for use in transaction construction. + */ +export class PythPriceClient { + private client: PythLazerClient | null = null; + private latestSolanaHex: string | null = null; + + constructor(private config: PythClientConfig) {} + + /** Create the WebSocket connection and start subscribing. */ + async connect(): Promise { + if (this.client) return; + + const wsUrl = this.config.wsUrl ?? DEFAULT_WS_URL; + + this.client = await PythLazerClient.create( + [wsUrl], + this.config.accessToken, + this.config.numConnections ?? 1, + ); + + const feedIds = this.config.feedIds ?? [ADA_USD_FEED_ID]; + + await this.client.subscribe({ + type: "subscribe", + subscriptionId: 1, + priceFeedIds: feedIds, + properties: ["price", "exponent"], + chains: ["solana"], + channel: "fixed_rate@200ms", + jsonBinaryEncoding: "hex", + parsed: false, + }); + + this.client.addMessageListener((message: JsonOrBinaryResponse) => { + if (message.type === "json") { + const val = message.value; + if (val.type === "streamUpdated" && val.solana) { + this.latestSolanaHex = val.solana.data; + } + } else if (message.type === "binary") { + if (message.value.solana) { + this.latestSolanaHex = message.value.solana.toString("hex"); + } + } + }); + } + + /** + * Get the latest raw price update as a Buffer for use as the + * withdraw-script redeemer in a Cardano transaction. + */ + getLatestUpdateBuffer(): Buffer { + if (!this.latestSolanaHex) { + throw new Error( + "No Pyth price update available yet. Call connect() first and wait for data.", + ); + } + return Buffer.from(this.latestSolanaHex, "hex"); + } + + /** Get the latest update as hex string. */ + getLatestUpdateHex(): string { + if (!this.latestSolanaHex) { + throw new Error( + "No Pyth price update available yet. Call connect() first and wait for data.", + ); + } + return this.latestSolanaHex; + } + + /** Check if we have received at least one price update. */ + hasUpdate(): boolean { + return this.latestSolanaHex !== null; + } + + /** Wait until the first price update arrives. */ + async waitForUpdate(timeoutMs = 10_000): Promise { + const start = Date.now(); + while (!this.hasUpdate()) { + if (Date.now() - start > timeoutMs) { + throw new Error("Timed out waiting for Pyth price update"); + } + await new Promise((r) => setTimeout(r, 100)); + } + } + + /** Shut down the WebSocket connections. */ + shutdown(): void { + this.client?.shutdown(); + this.client = null; + this.latestSolanaHex = null; + } +} + +// --- Transaction helpers --- + +/** + * Fetch the Pyth on-chain state and build all params needed for a Pyth tx. + * + * Usage with Evolution SDK: + * ```typescript + * const params = await preparePythTx(client, priceClient); + * + * const now = BigInt(Date.now()); + * const tx = wallet.newTx() + * .setValidity({ from: now - 60_000n, to: now + 60_000n }) + * .readFrom({ referenceInputs: [params.stateUtxo] }) + * .withdraw({ + * amount: 0n, + * redeemer: [params.updateBuffer], + * stakeCredential: ScriptHash.fromHex(params.withdrawScriptHash), + * }); + * ``` + */ +export interface PythTxParams { + stateUtxo: UTxO.UTxO; + withdrawScriptHash: string; + updateBuffer: Buffer; +} + +export async function preparePythTx( + cardanoClient: ProviderOnlyClient, + priceClient: PythPriceClient, + policyId: string = PYTH_PREPROD_POLICY_ID, +): Promise { + const stateUtxo = await getPythState(policyId, cardanoClient); + const withdrawScriptHash = getPythScriptHash(stateUtxo); + const updateBuffer = priceClient.getLatestUpdateBuffer(); + + return { stateUtxo, withdrawScriptHash, updateBuffer }; +} diff --git a/lazer/cardano/factura-ya/offchain/tsconfig.json b/lazer/cardano/factura-ya/offchain/tsconfig.json new file mode 100644 index 00000000..ff8c73b6 --- /dev/null +++ b/lazer/cardano/factura-ya/offchain/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true + }, + "include": ["src/**/*"] +} diff --git a/lazer/cardano/factura-ya/plutus.json b/lazer/cardano/factura-ya/plutus.json new file mode 100644 index 00000000..40491504 --- /dev/null +++ b/lazer/cardano/factura-ya/plutus.json @@ -0,0 +1,340 @@ +{ + "preamble": { + "title": "factura-ya/contracts", + "description": "Aiken contracts for project 'factura-ya/contracts'", + "version": "0.0.0", + "plutusVersion": "v3", + "compiler": { + "name": "Aiken", + "version": "v1.1.21+42babe5" + }, + "license": "Apache-2.0" + }, + "validators": [ + { + "title": "escrow.escrow.spend", + "datum": { + "title": "datum", + "schema": { + "$ref": "#/definitions/types~1EscrowDatum" + } + }, + "redeemer": { + "title": "redeemer", + "schema": { + "$ref": "#/definitions/types~1EscrowRedeemer" + } + }, + "compiledCode": "59036901010029800aba2aba1aab9faab9eaab9dab9a48888896600264653001300700198039804000cdc3a400530070024888966002600460106ea800e26466453001159800980098059baa0028cc004c038c030dd500148c03cc040c040c040c040c0400064601e6020602060200032232330010010032259800800c528456600266e3cdd71809000801c528c4cc008008c04c00500e20229180798081808180818081808180818081808000c8c966002600e601a6ea800626eb4c040c038dd5000c520004030601e601a6ea8c03cc034dd51807980818081808180818081808180818069baa001918079808180818081808000c8c03cc040c0400064601e6020002911111111192cc004c02cc054dd5008c56600266ebcc024c058dd500526103d87980008992cc004c030c058dd5000c566002660106eb0c01cc05cdd50079bae30193017375400315980099b89375a600a602e6ea802cc01803e2b30019800807cdd71801980b9baa00b9bad300430173754016801229462c80aa2c80aa2c80aa2c80a8c020c058dd5005459014456600266ebcc024c058dd5005260103d87980008992cc004c030c058dd5000c4c966002660126eb0c020c060dd5008000c56600266e24cdc01bad300630183754018904048726002180380845660033001010800cdd69802980c1baa00c400d14a31640591640591640586eb8c064c05cdd5000c590151804180b1baa00a8b2028405044464660020026eb0c018c064dd5002112cc00400629422b30013232598009808980d9baa0018acc004cdc79bae301e301c375400200d13371200a6466446600400400244b3001001801c4c8cc896600266e45221000028acc004cdc7a441000028800c01902044cc014014c0940110201bae301f001375a604000260420028100c8c8cc004004dd59805980f9baa0052259800800c00e2646644b3001337229101000028acc004cdc7a441000028800c01902144cc014014c0980110211bae3020001375660420026044002810852f5bded8c029000452820348a504068603a60366ea8c074c06cdd5000980e000c528c4cc008008c074005018203645900a4c02cdd5003cc03800d2225980098020014566002601e6ea802a00716404115980098040014566002601e6ea802a00716404116403480686018601a0026e1d20003009375400716401c30070013003375400f149a26cac8009", + "hash": "cb8368c843c59ac8d700cf647592c24b74fcca575fd8f42ff0793254" + }, + { + "title": "escrow.escrow.else", + "redeemer": { + "schema": {} + }, + "compiledCode": "59036901010029800aba2aba1aab9faab9eaab9dab9a48888896600264653001300700198039804000cdc3a400530070024888966002600460106ea800e26466453001159800980098059baa0028cc004c038c030dd500148c03cc040c040c040c040c0400064601e6020602060200032232330010010032259800800c528456600266e3cdd71809000801c528c4cc008008c04c00500e20229180798081808180818081808180818081808000c8c966002600e601a6ea800626eb4c040c038dd5000c520004030601e601a6ea8c03cc034dd51807980818081808180818081808180818069baa001918079808180818081808000c8c03cc040c0400064601e6020002911111111192cc004c02cc054dd5008c56600266ebcc024c058dd500526103d87980008992cc004c030c058dd5000c566002660106eb0c01cc05cdd50079bae30193017375400315980099b89375a600a602e6ea802cc01803e2b30019800807cdd71801980b9baa00b9bad300430173754016801229462c80aa2c80aa2c80aa2c80a8c020c058dd5005459014456600266ebcc024c058dd5005260103d87980008992cc004c030c058dd5000c4c966002660126eb0c020c060dd5008000c56600266e24cdc01bad300630183754018904048726002180380845660033001010800cdd69802980c1baa00c400d14a31640591640591640586eb8c064c05cdd5000c590151804180b1baa00a8b2028405044464660020026eb0c018c064dd5002112cc00400629422b30013232598009808980d9baa0018acc004cdc79bae301e301c375400200d13371200a6466446600400400244b3001001801c4c8cc896600266e45221000028acc004cdc7a441000028800c01902044cc014014c0940110201bae301f001375a604000260420028100c8c8cc004004dd59805980f9baa0052259800800c00e2646644b3001337229101000028acc004cdc7a441000028800c01902144cc014014c0980110211bae3020001375660420026044002810852f5bded8c029000452820348a504068603a60366ea8c074c06cdd5000980e000c528c4cc008008c074005018203645900a4c02cdd5003cc03800d2225980098020014566002601e6ea802a00716404115980098040014566002601e6ea802a00716404116403480686018601a0026e1d20003009375400716401c30070013003375400f149a26cac8009", + "hash": "cb8368c843c59ac8d700cf647592c24b74fcca575fd8f42ff0793254" + }, + { + "title": "invoice_mint.invoice_mint.mint", + "redeemer": { + "title": "redeemer", + "schema": { + "$ref": "#/definitions/types~1InvoiceMintRedeemer" + } + }, + "parameters": [ + { + "title": "escrow_script_hash", + "schema": { + "$ref": "#/definitions/ByteArray" + } + } + ], + "compiledCode": "590607010100229800aba2aba1aba0aab9faab9eaab9dab9a9bae0024888888896600264653001300900198049805000cdc3a400130090024888966002600460126ea800e266446644b300130060018acc004c038dd5004400a2c807a2b300130030018acc004c038dd5004400a2c807a2c806100c0cc0048c040c044c044c044c04400644646600200200644b30010018a508acc004cdc79bae30130010038a51899801001180a000a01c4045230103011301130113011301130113011301100191808180898089808800c8c040c044c044c044c044c0440064602060220032232330010010032259800800c5300103d87a80008992cc004c010006266e952000330130014bd7044cc00c00cc05400900f1809800a0229180818089808800c88c8c8cc004004010896600200300389919912cc004cdc8803801456600266e3c01c00a20030064049133005005301800440486eb8c044004dd59809000980a000a02414bd6f7b6304dc3a400891111111112cc004c038c054dd500844c9660026036003132325980098071bad30190028992cc004cc034dd61806180d1baa011375c601260346ea80062b30013371090001bad3007301a375400315980099b8832598009808180d1baa00189bad301e301b3754003148001019180e980d1baa301d301a3754603a603c603c603c603c603c603c603c60346ea8044dd69805980d1baa0018acc006600266e3cdd71805180d1baa001488100a50a51406115980099b8f375c603a60346ea800400a29462c80c22c80c22c80c22c80c22c80c0c966002602460326ea8006264b30013006301a375400313259800980a180d9baa0018991919191919194c004dd69813000cdd71813003cdd718130034dd69813002cdd698130024dd71813001cdd7181300124444444b3001302e0088807c5902b0c098004c094004c090004c08c004c088004c084004c070dd5000c5901a180f180d9baa0018b20323007301a3754603a60346ea80062c80c0cc01cdd61803180c9baa0102300f323322330020020012259800800c00e2646644b30013372201000515980099b8f0080028800c01901e44cc014014c09001101e1bae301d001375a603c002604000280f0cc01cdd59805180d9baa0020111480022c80b8dd7180b800980d000c59018198011bab300a3016375401a01919800912cc004c040c05cdd500144c8c8c8c8c8ca60026042003375c604200d375c604200b375a6042009375a6042004911112cc004c09c01a2646644b3001301e002899192cc004c0b000a0071640a46eb8c0a8004c098dd5001c566002603600515980098131baa003800c59027459024204830233754002264b3001301d0018acc004c094dd5003c03a2c81322b3001301a0018acc004c094dd5003c03a2c81322b300130100018acc004c094dd5003c03a2c81322c811902320463023375400c604c01116409030210013020001301f001301e001301d001301837540051640592232330010010032259800800c52f5c113301c3003301d00133002002301e001406d3300237566014602c6ea803403122259800980e800c4c96600266e1cdd6980d000a400313259800acc004cdd79805980d9baa0014c0103d87a80008a51899baf300b301b375400298103d87b8000406513259800980a180d9baa0018998079bac300e301c37540266eb8c07cc070dd5000c4cc03cdd61807180e1baa013375c601660386ea800901a1806180d9baa0018b2032323259800980a180d9baa0018992cc004c020c070dd5000c4c8cc0200044004c080c074dd5000c5901b1804980e1baa301f301c3754003164068660126466446600400400244b30010018801c4cc080c084004cc008008c08800501f198029bac300b301c375402646018603a6ea8004cc010dd6180f180d9baa0122300b301c3754002464b30013012301c375400315980099b8f375c6040603a6ea800406a264b30013009301d37540031323300900113371e6eb8c088c07cdd50008029810980f1baa0018a5040706014603a6ea800a294101b45282036301f301c3754603e60386ea8004dd7180c800c59018180e000c5901a10140c02cdd50031bae300d300a37540066e1d20028b2010180480098021baa0098a4d13656400801", + "hash": "30268933aae8a060641fa0fb2417b675159dd2e74cc6eae65ad3b813" + }, + { + "title": "invoice_mint.invoice_mint.else", + "redeemer": { + "schema": {} + }, + "parameters": [ + { + "title": "escrow_script_hash", + "schema": { + "$ref": "#/definitions/ByteArray" + } + } + ], + "compiledCode": "590607010100229800aba2aba1aba0aab9faab9eaab9dab9a9bae0024888888896600264653001300900198049805000cdc3a400130090024888966002600460126ea800e266446644b300130060018acc004c038dd5004400a2c807a2b300130030018acc004c038dd5004400a2c807a2c806100c0cc0048c040c044c044c044c04400644646600200200644b30010018a508acc004cdc79bae30130010038a51899801001180a000a01c4045230103011301130113011301130113011301100191808180898089808800c8c040c044c044c044c044c0440064602060220032232330010010032259800800c5300103d87a80008992cc004c010006266e952000330130014bd7044cc00c00cc05400900f1809800a0229180818089808800c88c8c8cc004004010896600200300389919912cc004cdc8803801456600266e3c01c00a20030064049133005005301800440486eb8c044004dd59809000980a000a02414bd6f7b6304dc3a400891111111112cc004c038c054dd500844c9660026036003132325980098071bad30190028992cc004cc034dd61806180d1baa011375c601260346ea80062b30013371090001bad3007301a375400315980099b8832598009808180d1baa00189bad301e301b3754003148001019180e980d1baa301d301a3754603a603c603c603c603c603c603c603c60346ea8044dd69805980d1baa0018acc006600266e3cdd71805180d1baa001488100a50a51406115980099b8f375c603a60346ea800400a29462c80c22c80c22c80c22c80c22c80c0c966002602460326ea8006264b30013006301a375400313259800980a180d9baa0018991919191919194c004dd69813000cdd71813003cdd718130034dd69813002cdd698130024dd71813001cdd7181300124444444b3001302e0088807c5902b0c098004c094004c090004c08c004c088004c084004c070dd5000c5901a180f180d9baa0018b20323007301a3754603a60346ea80062c80c0cc01cdd61803180c9baa0102300f323322330020020012259800800c00e2646644b30013372201000515980099b8f0080028800c01901e44cc014014c09001101e1bae301d001375a603c002604000280f0cc01cdd59805180d9baa0020111480022c80b8dd7180b800980d000c59018198011bab300a3016375401a01919800912cc004c040c05cdd500144c8c8c8c8c8ca60026042003375c604200d375c604200b375a6042009375a6042004911112cc004c09c01a2646644b3001301e002899192cc004c0b000a0071640a46eb8c0a8004c098dd5001c566002603600515980098131baa003800c59027459024204830233754002264b3001301d0018acc004c094dd5003c03a2c81322b3001301a0018acc004c094dd5003c03a2c81322b300130100018acc004c094dd5003c03a2c81322c811902320463023375400c604c01116409030210013020001301f001301e001301d001301837540051640592232330010010032259800800c52f5c113301c3003301d00133002002301e001406d3300237566014602c6ea803403122259800980e800c4c96600266e1cdd6980d000a400313259800acc004cdd79805980d9baa0014c0103d87a80008a51899baf300b301b375400298103d87b8000406513259800980a180d9baa0018998079bac300e301c37540266eb8c07cc070dd5000c4cc03cdd61807180e1baa013375c601660386ea800901a1806180d9baa0018b2032323259800980a180d9baa0018992cc004c020c070dd5000c4c8cc0200044004c080c074dd5000c5901b1804980e1baa301f301c3754003164068660126466446600400400244b30010018801c4cc080c084004cc008008c08800501f198029bac300b301c375402646018603a6ea8004cc010dd6180f180d9baa0122300b301c3754002464b30013012301c375400315980099b8f375c6040603a6ea800406a264b30013009301d37540031323300900113371e6eb8c088c07cdd50008029810980f1baa0018a5040706014603a6ea800a294101b45282036301f301c3754603e60386ea8004dd7180c800c59018180e000c5901a10140c02cdd50031bae300d300a37540066e1d20028b2010180480098021baa0098a4d13656400801", + "hash": "30268933aae8a060641fa0fb2417b675159dd2e74cc6eae65ad3b813" + }, + { + "title": "marketplace.marketplace.spend", + "datum": { + "title": "datum", + "schema": { + "$ref": "#/definitions/types~1ListingDatum" + } + }, + "redeemer": { + "title": "redeemer", + "schema": { + "$ref": "#/definitions/types~1MarketplaceRedeemer" + } + }, + "parameters": [ + { + "title": "invoice_policy_id", + "schema": { + "$ref": "#/definitions/ByteArray" + } + }, + { + "title": "escrow_script_hash", + "schema": { + "$ref": "#/definitions/ByteArray" + } + } + ], + "compiledCode": "5905290101002229800aba2aba1aba0aab9faab9eaab9dab9a9bae0039bae00248888888896600264653001300a00198051805800cdc3a4005300a0024888966002600460146ea800e26466453001159800980098069baa0028cc004c044c038dd500148c048c04cc04cc04cc04cc04cc04cc04c00646024602660266026602660266026602660260032232330010010032259800800c52845660026006602a00314a313300200230160014040809a4602460266026003222323322330020020012259800800c00e2646644b30013372200e00515980099b8f0070028800c01901544cc014014c06c0110151bae3014001375a602a002602e00280a8c8c8cc004004018896600200300389919912cc004cdc8804801456600266e3c02400a20030064059133005005301c00440586eb8c054004dd5980b000980c000a02c14bd6f7b6300a40012301230130019baf4c103d8798000488888888c9660026014602c6ea80422b300130023008301737540131598009991198041bac30073019375401e464b3001300e301a375400315980099b8f375c603c60366ea8004012266e2400e60026eacc01cc06cdd50015220100a44100402114a080ca2941019180e980d1baa301d301a37540026eb8c00cc05cdd50049bad300530173754013132598009805980b9baa0018992cc006600201f0169bae301c30193754017001400d15980099198049bac3008301a3754020464b30013013301b375400315980099b8f375c603e60386ea8004062264b30013370e9002180e1baa001899192cc004c048c078dd500144c8c8c8c8c8ca60026050003375c605000d375c605000b375a6050009375a6050004911112cc004c0b801a2646644b30013020002899192cc004c0cc00a0071640c06eb8c0c4004c0b4dd5001c566002604800515980098169baa003800c5902e45902b2056302a3754002264b3001301f0018acc004c0b0dd5003c03a2c816a2b300130230018acc004c0b0dd5003c03a2c816a2b30013370e9002000c56600260586ea801e01d1640b51640a8815102a18151baa006302d0088b205618140009813800981300098128009812000980f9baa0028b203a15980099b8f375c6042603c6ea80040162b30013375e6042604460446044603c6ea8004cdd2a4000660406ea40192f5c113009302130223022302230223022301e375400314a080e2294101c1810180e9baa0018a50406c601460386ea800a294101a45282034301e301b3754603c60366ea8004dd7180e180c9baa00b8a518b202e8b202e375c603660306ea80062c80b0c8cc004004dd61804180c1baa00e2259800800c5300103d87a80008992cc006600266e3c004dd71803180d1baa00ca50a51406113374a90001980e1ba90014bd7044cc00c00cc0780090181bae301c001406916405516405515980098011804180b9baa0098acc004c8cc88cc008008004896600200314a115980099b8f375c603a00200714a3133002002301e001406080d8dd61804180c1baa00e375c6006602e6ea80262b30019800806c0526eb8c068c05cdd5004cdd71801980b9baa009400514a316405516405516405480a88888cc024dd61804180d1baa004232598009807980d9baa0018acc004cdc79bae301f301c37540020071301398009bab3008301c375400500580220128a50406914a080d0c078c06cdd5180f180d9baa00145900c4c034dd5003cc04400d222598009802001456600260226ea802a0071640491598009804001456600260226ea802a00716404916403c8078601e60200026e1d2000300b3754007164024300a00130053754015149a26cac80181", + "hash": "2f18f39c41ea3cfa94d3e0cbbc0021d53b0ed0ae7392e428e00ad402" + }, + { + "title": "marketplace.marketplace.else", + "redeemer": { + "schema": {} + }, + "parameters": [ + { + "title": "invoice_policy_id", + "schema": { + "$ref": "#/definitions/ByteArray" + } + }, + { + "title": "escrow_script_hash", + "schema": { + "$ref": "#/definitions/ByteArray" + } + } + ], + "compiledCode": "5905290101002229800aba2aba1aba0aab9faab9eaab9dab9a9bae0039bae00248888888896600264653001300a00198051805800cdc3a4005300a0024888966002600460146ea800e26466453001159800980098069baa0028cc004c044c038dd500148c048c04cc04cc04cc04cc04cc04cc04c00646024602660266026602660266026602660260032232330010010032259800800c52845660026006602a00314a313300200230160014040809a4602460266026003222323322330020020012259800800c00e2646644b30013372200e00515980099b8f0070028800c01901544cc014014c06c0110151bae3014001375a602a002602e00280a8c8c8cc004004018896600200300389919912cc004cdc8804801456600266e3c02400a20030064059133005005301c00440586eb8c054004dd5980b000980c000a02c14bd6f7b6300a40012301230130019baf4c103d8798000488888888c9660026014602c6ea80422b300130023008301737540131598009991198041bac30073019375401e464b3001300e301a375400315980099b8f375c603c60366ea8004012266e2400e60026eacc01cc06cdd50015220100a44100402114a080ca2941019180e980d1baa301d301a37540026eb8c00cc05cdd50049bad300530173754013132598009805980b9baa0018992cc006600201f0169bae301c30193754017001400d15980099198049bac3008301a3754020464b30013013301b375400315980099b8f375c603e60386ea8004062264b30013370e9002180e1baa001899192cc004c048c078dd500144c8c8c8c8c8ca60026050003375c605000d375c605000b375a6050009375a6050004911112cc004c0b801a2646644b30013020002899192cc004c0cc00a0071640c06eb8c0c4004c0b4dd5001c566002604800515980098169baa003800c5902e45902b2056302a3754002264b3001301f0018acc004c0b0dd5003c03a2c816a2b300130230018acc004c0b0dd5003c03a2c816a2b30013370e9002000c56600260586ea801e01d1640b51640a8815102a18151baa006302d0088b205618140009813800981300098128009812000980f9baa0028b203a15980099b8f375c6042603c6ea80040162b30013375e6042604460446044603c6ea8004cdd2a4000660406ea40192f5c113009302130223022302230223022301e375400314a080e2294101c1810180e9baa0018a50406c601460386ea800a294101a45282034301e301b3754603c60366ea8004dd7180e180c9baa00b8a518b202e8b202e375c603660306ea80062c80b0c8cc004004dd61804180c1baa00e2259800800c5300103d87a80008992cc006600266e3c004dd71803180d1baa00ca50a51406113374a90001980e1ba90014bd7044cc00c00cc0780090181bae301c001406916405516405515980098011804180b9baa0098acc004c8cc88cc008008004896600200314a115980099b8f375c603a00200714a3133002002301e001406080d8dd61804180c1baa00e375c6006602e6ea80262b30019800806c0526eb8c068c05cdd5004cdd71801980b9baa009400514a316405516405516405480a88888cc024dd61804180d1baa004232598009807980d9baa0018acc004cdc79bae301f301c37540020071301398009bab3008301c375400500580220128a50406914a080d0c078c06cdd5180f180d9baa00145900c4c034dd5003cc04400d222598009802001456600260226ea802a0071640491598009804001456600260226ea802a00716404916403c8078601e60200026e1d2000300b3754007164024300a00130053754015149a26cac80181", + "hash": "2f18f39c41ea3cfa94d3e0cbbc0021d53b0ed0ae7392e428e00ad402" + } + ], + "definitions": { + "ByteArray": { + "dataType": "bytes" + }, + "Int": { + "dataType": "integer" + }, + "Option": { + "title": "Option", + "anyOf": [ + { + "title": "Some", + "description": "An optional value.", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "$ref": "#/definitions/aiken~1crypto~1VerificationKeyHash" + } + ] + }, + { + "title": "None", + "description": "Nothing.", + "dataType": "constructor", + "index": 1, + "fields": [] + } + ] + }, + "aiken/crypto/VerificationKeyHash": { + "title": "VerificationKeyHash", + "dataType": "bytes" + }, + "types/EscrowDatum": { + "title": "EscrowDatum", + "anyOf": [ + { + "title": "EscrowDatum", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "invoice_id", + "$ref": "#/definitions/ByteArray" + }, + { + "title": "seller", + "$ref": "#/definitions/aiken~1crypto~1VerificationKeyHash" + }, + { + "title": "collateral_lovelace", + "$ref": "#/definitions/Int" + }, + { + "title": "buyer", + "$ref": "#/definitions/Option" + }, + { + "title": "due_date_posix_ms", + "$ref": "#/definitions/Int" + }, + { + "title": "status", + "$ref": "#/definitions/types~1EscrowStatus" + } + ] + } + ] + }, + "types/EscrowRedeemer": { + "title": "EscrowRedeemer", + "anyOf": [ + { + "title": "Release", + "dataType": "constructor", + "index": 0, + "fields": [] + }, + { + "title": "Forfeit", + "dataType": "constructor", + "index": 1, + "fields": [] + } + ] + }, + "types/EscrowStatus": { + "title": "EscrowStatus", + "anyOf": [ + { + "title": "Locked", + "dataType": "constructor", + "index": 0, + "fields": [] + }, + { + "title": "Released", + "dataType": "constructor", + "index": 1, + "fields": [] + }, + { + "title": "Forfeited", + "dataType": "constructor", + "index": 2, + "fields": [] + } + ] + }, + "types/InvoiceMintRedeemer": { + "title": "InvoiceMintRedeemer", + "anyOf": [ + { + "title": "MintInvoice", + "dataType": "constructor", + "index": 0, + "fields": [] + }, + { + "title": "BurnInvoice", + "dataType": "constructor", + "index": 1, + "fields": [] + } + ] + }, + "types/ListingDatum": { + "title": "ListingDatum", + "anyOf": [ + { + "title": "ListingDatum", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "invoice_id", + "$ref": "#/definitions/ByteArray" + }, + { + "title": "seller", + "$ref": "#/definitions/aiken~1crypto~1VerificationKeyHash" + }, + { + "title": "asking_price_lovelace", + "$ref": "#/definitions/Int" + }, + { + "title": "original_amount_ars", + "$ref": "#/definitions/Int" + }, + { + "title": "pyth_price_at_listing", + "$ref": "#/definitions/Int" + }, + { + "title": "pyth_exponent_at_listing", + "$ref": "#/definitions/Int" + }, + { + "title": "due_date_posix_ms", + "$ref": "#/definitions/Int" + }, + { + "title": "status", + "$ref": "#/definitions/types~1ListingStatus" + } + ] + } + ] + }, + "types/ListingStatus": { + "title": "ListingStatus", + "anyOf": [ + { + "title": "Listed", + "dataType": "constructor", + "index": 0, + "fields": [] + }, + { + "title": "Sold", + "dataType": "constructor", + "index": 1, + "fields": [] + }, + { + "title": "Delisted", + "dataType": "constructor", + "index": 2, + "fields": [] + } + ] + }, + "types/MarketplaceRedeemer": { + "title": "MarketplaceRedeemer", + "anyOf": [ + { + "title": "Purchase", + "dataType": "constructor", + "index": 0, + "fields": [] + }, + { + "title": "Delist", + "dataType": "constructor", + "index": 1, + "fields": [] + } + ] + } + } +} \ No newline at end of file From 7be4837d69d29dfea4533ee3e719ea724a80b783 Mon Sep 17 00:00:00 2001 From: Quimey Lucas Marquez Date: Sun, 22 Mar 2026 15:21:40 -0300 Subject: [PATCH 2/8] =?UTF-8?q?chore:=20sync=20latest=20source=20=E2=80=94?= =?UTF-8?q?=20deploy=20server,=20wallet=20verification,=20Node=2020?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- lazer/cardano/factura-ya/.nvmrc | 1 + .../cardano/factura-ya/frontend/package.json | 10 +- .../factura-ya/frontend/public/deploy.html | 153 ++++++++ .../factura-ya/frontend/public/plutus.json | 340 ++++++++++++++++++ lazer/cardano/factura-ya/frontend/src/App.tsx | 14 +- .../frontend/src/components/DemoGuide.tsx | 135 +++++++ .../frontend/src/components/Deploy.tsx | 43 +++ .../frontend/src/components/Marketplace.tsx | 159 ++++---- .../frontend/src/components/PriceDisplay.tsx | 23 +- .../src/components/RegisterInvoice.tsx | 17 +- .../frontend/src/components/WalletConnect.tsx | 2 +- .../cardano/factura-ya/frontend/src/index.css | 80 +++++ .../frontend/src/lib/transactions.ts | 135 ++----- .../factura-ya/frontend/src/lib/wallet.ts | 147 ++++++-- .../factura-ya/frontend/vite.config.ts | 5 + lazer/cardano/factura-ya/indexer/src/api.ts | 15 - .../cardano/factura-ya/offchain/package.json | 4 + .../cardano/factura-ya/offchain/src/client.ts | 45 +++ .../offchain/src/deploy-bundle-entry.ts | 8 + .../factura-ya/offchain/src/deploy-server.ts | 205 +++++++++++ .../cardano/factura-ya/offchain/src/deploy.ts | 148 ++++++++ 21 files changed, 1434 insertions(+), 255 deletions(-) create mode 100644 lazer/cardano/factura-ya/.nvmrc create mode 100644 lazer/cardano/factura-ya/frontend/public/deploy.html create mode 100644 lazer/cardano/factura-ya/frontend/public/plutus.json create mode 100644 lazer/cardano/factura-ya/frontend/src/components/DemoGuide.tsx create mode 100644 lazer/cardano/factura-ya/frontend/src/components/Deploy.tsx create mode 100644 lazer/cardano/factura-ya/offchain/src/client.ts create mode 100644 lazer/cardano/factura-ya/offchain/src/deploy-bundle-entry.ts create mode 100644 lazer/cardano/factura-ya/offchain/src/deploy-server.ts create mode 100644 lazer/cardano/factura-ya/offchain/src/deploy.ts diff --git a/lazer/cardano/factura-ya/.nvmrc b/lazer/cardano/factura-ya/.nvmrc new file mode 100644 index 00000000..209e3ef4 --- /dev/null +++ b/lazer/cardano/factura-ya/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/lazer/cardano/factura-ya/frontend/package.json b/lazer/cardano/factura-ya/frontend/package.json index 605f44e0..8c6f3eea 100644 --- a/lazer/cardano/factura-ya/frontend/package.json +++ b/lazer/cardano/factura-ya/frontend/package.json @@ -13,10 +13,10 @@ "react-dom": "^18.3.1" }, "devDependencies": { - "@types/react": "^18.3.0", - "@types/react-dom": "^18.3.0", - "@vitejs/plugin-react": "^4.3.0", - "typescript": "^5.4.0", - "vite": "^5.4.0" + "@types/react": "^18.3.28", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react": "^4.7.0", + "typescript": "^5.9.3", + "vite": "^5.4.21" } } diff --git a/lazer/cardano/factura-ya/frontend/public/deploy.html b/lazer/cardano/factura-ya/frontend/public/deploy.html new file mode 100644 index 00000000..abd5bdc9 --- /dev/null +++ b/lazer/cardano/factura-ya/frontend/public/deploy.html @@ -0,0 +1,153 @@ + + + + + Factura Ya — Deploy to PreProd + + + +

Factura Ya — Deploy Validators

+

Select wallet and deploy 3 reference scripts to PreProd.

+

Cost: ~40 tADA (locked in UTxOs, recoverable)

+
+ +
+ +
+ + + + diff --git a/lazer/cardano/factura-ya/frontend/public/plutus.json b/lazer/cardano/factura-ya/frontend/public/plutus.json new file mode 100644 index 00000000..40491504 --- /dev/null +++ b/lazer/cardano/factura-ya/frontend/public/plutus.json @@ -0,0 +1,340 @@ +{ + "preamble": { + "title": "factura-ya/contracts", + "description": "Aiken contracts for project 'factura-ya/contracts'", + "version": "0.0.0", + "plutusVersion": "v3", + "compiler": { + "name": "Aiken", + "version": "v1.1.21+42babe5" + }, + "license": "Apache-2.0" + }, + "validators": [ + { + "title": "escrow.escrow.spend", + "datum": { + "title": "datum", + "schema": { + "$ref": "#/definitions/types~1EscrowDatum" + } + }, + "redeemer": { + "title": "redeemer", + "schema": { + "$ref": "#/definitions/types~1EscrowRedeemer" + } + }, + "compiledCode": "59036901010029800aba2aba1aab9faab9eaab9dab9a48888896600264653001300700198039804000cdc3a400530070024888966002600460106ea800e26466453001159800980098059baa0028cc004c038c030dd500148c03cc040c040c040c040c0400064601e6020602060200032232330010010032259800800c528456600266e3cdd71809000801c528c4cc008008c04c00500e20229180798081808180818081808180818081808000c8c966002600e601a6ea800626eb4c040c038dd5000c520004030601e601a6ea8c03cc034dd51807980818081808180818081808180818069baa001918079808180818081808000c8c03cc040c0400064601e6020002911111111192cc004c02cc054dd5008c56600266ebcc024c058dd500526103d87980008992cc004c030c058dd5000c566002660106eb0c01cc05cdd50079bae30193017375400315980099b89375a600a602e6ea802cc01803e2b30019800807cdd71801980b9baa00b9bad300430173754016801229462c80aa2c80aa2c80aa2c80a8c020c058dd5005459014456600266ebcc024c058dd5005260103d87980008992cc004c030c058dd5000c4c966002660126eb0c020c060dd5008000c56600266e24cdc01bad300630183754018904048726002180380845660033001010800cdd69802980c1baa00c400d14a31640591640591640586eb8c064c05cdd5000c590151804180b1baa00a8b2028405044464660020026eb0c018c064dd5002112cc00400629422b30013232598009808980d9baa0018acc004cdc79bae301e301c375400200d13371200a6466446600400400244b3001001801c4c8cc896600266e45221000028acc004cdc7a441000028800c01902044cc014014c0940110201bae301f001375a604000260420028100c8c8cc004004dd59805980f9baa0052259800800c00e2646644b3001337229101000028acc004cdc7a441000028800c01902144cc014014c0980110211bae3020001375660420026044002810852f5bded8c029000452820348a504068603a60366ea8c074c06cdd5000980e000c528c4cc008008c074005018203645900a4c02cdd5003cc03800d2225980098020014566002601e6ea802a00716404115980098040014566002601e6ea802a00716404116403480686018601a0026e1d20003009375400716401c30070013003375400f149a26cac8009", + "hash": "cb8368c843c59ac8d700cf647592c24b74fcca575fd8f42ff0793254" + }, + { + "title": "escrow.escrow.else", + "redeemer": { + "schema": {} + }, + "compiledCode": "59036901010029800aba2aba1aab9faab9eaab9dab9a48888896600264653001300700198039804000cdc3a400530070024888966002600460106ea800e26466453001159800980098059baa0028cc004c038c030dd500148c03cc040c040c040c040c0400064601e6020602060200032232330010010032259800800c528456600266e3cdd71809000801c528c4cc008008c04c00500e20229180798081808180818081808180818081808000c8c966002600e601a6ea800626eb4c040c038dd5000c520004030601e601a6ea8c03cc034dd51807980818081808180818081808180818069baa001918079808180818081808000c8c03cc040c0400064601e6020002911111111192cc004c02cc054dd5008c56600266ebcc024c058dd500526103d87980008992cc004c030c058dd5000c566002660106eb0c01cc05cdd50079bae30193017375400315980099b89375a600a602e6ea802cc01803e2b30019800807cdd71801980b9baa00b9bad300430173754016801229462c80aa2c80aa2c80aa2c80a8c020c058dd5005459014456600266ebcc024c058dd5005260103d87980008992cc004c030c058dd5000c4c966002660126eb0c020c060dd5008000c56600266e24cdc01bad300630183754018904048726002180380845660033001010800cdd69802980c1baa00c400d14a31640591640591640586eb8c064c05cdd5000c590151804180b1baa00a8b2028405044464660020026eb0c018c064dd5002112cc00400629422b30013232598009808980d9baa0018acc004cdc79bae301e301c375400200d13371200a6466446600400400244b3001001801c4c8cc896600266e45221000028acc004cdc7a441000028800c01902044cc014014c0940110201bae301f001375a604000260420028100c8c8cc004004dd59805980f9baa0052259800800c00e2646644b3001337229101000028acc004cdc7a441000028800c01902144cc014014c0980110211bae3020001375660420026044002810852f5bded8c029000452820348a504068603a60366ea8c074c06cdd5000980e000c528c4cc008008c074005018203645900a4c02cdd5003cc03800d2225980098020014566002601e6ea802a00716404115980098040014566002601e6ea802a00716404116403480686018601a0026e1d20003009375400716401c30070013003375400f149a26cac8009", + "hash": "cb8368c843c59ac8d700cf647592c24b74fcca575fd8f42ff0793254" + }, + { + "title": "invoice_mint.invoice_mint.mint", + "redeemer": { + "title": "redeemer", + "schema": { + "$ref": "#/definitions/types~1InvoiceMintRedeemer" + } + }, + "parameters": [ + { + "title": "escrow_script_hash", + "schema": { + "$ref": "#/definitions/ByteArray" + } + } + ], + "compiledCode": "590607010100229800aba2aba1aba0aab9faab9eaab9dab9a9bae0024888888896600264653001300900198049805000cdc3a400130090024888966002600460126ea800e266446644b300130060018acc004c038dd5004400a2c807a2b300130030018acc004c038dd5004400a2c807a2c806100c0cc0048c040c044c044c044c04400644646600200200644b30010018a508acc004cdc79bae30130010038a51899801001180a000a01c4045230103011301130113011301130113011301100191808180898089808800c8c040c044c044c044c044c0440064602060220032232330010010032259800800c5300103d87a80008992cc004c010006266e952000330130014bd7044cc00c00cc05400900f1809800a0229180818089808800c88c8c8cc004004010896600200300389919912cc004cdc8803801456600266e3c01c00a20030064049133005005301800440486eb8c044004dd59809000980a000a02414bd6f7b6304dc3a400891111111112cc004c038c054dd500844c9660026036003132325980098071bad30190028992cc004cc034dd61806180d1baa011375c601260346ea80062b30013371090001bad3007301a375400315980099b8832598009808180d1baa00189bad301e301b3754003148001019180e980d1baa301d301a3754603a603c603c603c603c603c603c603c60346ea8044dd69805980d1baa0018acc006600266e3cdd71805180d1baa001488100a50a51406115980099b8f375c603a60346ea800400a29462c80c22c80c22c80c22c80c22c80c0c966002602460326ea8006264b30013006301a375400313259800980a180d9baa0018991919191919194c004dd69813000cdd71813003cdd718130034dd69813002cdd698130024dd71813001cdd7181300124444444b3001302e0088807c5902b0c098004c094004c090004c08c004c088004c084004c070dd5000c5901a180f180d9baa0018b20323007301a3754603a60346ea80062c80c0cc01cdd61803180c9baa0102300f323322330020020012259800800c00e2646644b30013372201000515980099b8f0080028800c01901e44cc014014c09001101e1bae301d001375a603c002604000280f0cc01cdd59805180d9baa0020111480022c80b8dd7180b800980d000c59018198011bab300a3016375401a01919800912cc004c040c05cdd500144c8c8c8c8c8ca60026042003375c604200d375c604200b375a6042009375a6042004911112cc004c09c01a2646644b3001301e002899192cc004c0b000a0071640a46eb8c0a8004c098dd5001c566002603600515980098131baa003800c59027459024204830233754002264b3001301d0018acc004c094dd5003c03a2c81322b3001301a0018acc004c094dd5003c03a2c81322b300130100018acc004c094dd5003c03a2c81322c811902320463023375400c604c01116409030210013020001301f001301e001301d001301837540051640592232330010010032259800800c52f5c113301c3003301d00133002002301e001406d3300237566014602c6ea803403122259800980e800c4c96600266e1cdd6980d000a400313259800acc004cdd79805980d9baa0014c0103d87a80008a51899baf300b301b375400298103d87b8000406513259800980a180d9baa0018998079bac300e301c37540266eb8c07cc070dd5000c4cc03cdd61807180e1baa013375c601660386ea800901a1806180d9baa0018b2032323259800980a180d9baa0018992cc004c020c070dd5000c4c8cc0200044004c080c074dd5000c5901b1804980e1baa301f301c3754003164068660126466446600400400244b30010018801c4cc080c084004cc008008c08800501f198029bac300b301c375402646018603a6ea8004cc010dd6180f180d9baa0122300b301c3754002464b30013012301c375400315980099b8f375c6040603a6ea800406a264b30013009301d37540031323300900113371e6eb8c088c07cdd50008029810980f1baa0018a5040706014603a6ea800a294101b45282036301f301c3754603e60386ea8004dd7180c800c59018180e000c5901a10140c02cdd50031bae300d300a37540066e1d20028b2010180480098021baa0098a4d13656400801", + "hash": "30268933aae8a060641fa0fb2417b675159dd2e74cc6eae65ad3b813" + }, + { + "title": "invoice_mint.invoice_mint.else", + "redeemer": { + "schema": {} + }, + "parameters": [ + { + "title": "escrow_script_hash", + "schema": { + "$ref": "#/definitions/ByteArray" + } + } + ], + "compiledCode": "590607010100229800aba2aba1aba0aab9faab9eaab9dab9a9bae0024888888896600264653001300900198049805000cdc3a400130090024888966002600460126ea800e266446644b300130060018acc004c038dd5004400a2c807a2b300130030018acc004c038dd5004400a2c807a2c806100c0cc0048c040c044c044c044c04400644646600200200644b30010018a508acc004cdc79bae30130010038a51899801001180a000a01c4045230103011301130113011301130113011301100191808180898089808800c8c040c044c044c044c044c0440064602060220032232330010010032259800800c5300103d87a80008992cc004c010006266e952000330130014bd7044cc00c00cc05400900f1809800a0229180818089808800c88c8c8cc004004010896600200300389919912cc004cdc8803801456600266e3c01c00a20030064049133005005301800440486eb8c044004dd59809000980a000a02414bd6f7b6304dc3a400891111111112cc004c038c054dd500844c9660026036003132325980098071bad30190028992cc004cc034dd61806180d1baa011375c601260346ea80062b30013371090001bad3007301a375400315980099b8832598009808180d1baa00189bad301e301b3754003148001019180e980d1baa301d301a3754603a603c603c603c603c603c603c603c60346ea8044dd69805980d1baa0018acc006600266e3cdd71805180d1baa001488100a50a51406115980099b8f375c603a60346ea800400a29462c80c22c80c22c80c22c80c22c80c0c966002602460326ea8006264b30013006301a375400313259800980a180d9baa0018991919191919194c004dd69813000cdd71813003cdd718130034dd69813002cdd698130024dd71813001cdd7181300124444444b3001302e0088807c5902b0c098004c094004c090004c08c004c088004c084004c070dd5000c5901a180f180d9baa0018b20323007301a3754603a60346ea80062c80c0cc01cdd61803180c9baa0102300f323322330020020012259800800c00e2646644b30013372201000515980099b8f0080028800c01901e44cc014014c09001101e1bae301d001375a603c002604000280f0cc01cdd59805180d9baa0020111480022c80b8dd7180b800980d000c59018198011bab300a3016375401a01919800912cc004c040c05cdd500144c8c8c8c8c8ca60026042003375c604200d375c604200b375a6042009375a6042004911112cc004c09c01a2646644b3001301e002899192cc004c0b000a0071640a46eb8c0a8004c098dd5001c566002603600515980098131baa003800c59027459024204830233754002264b3001301d0018acc004c094dd5003c03a2c81322b3001301a0018acc004c094dd5003c03a2c81322b300130100018acc004c094dd5003c03a2c81322c811902320463023375400c604c01116409030210013020001301f001301e001301d001301837540051640592232330010010032259800800c52f5c113301c3003301d00133002002301e001406d3300237566014602c6ea803403122259800980e800c4c96600266e1cdd6980d000a400313259800acc004cdd79805980d9baa0014c0103d87a80008a51899baf300b301b375400298103d87b8000406513259800980a180d9baa0018998079bac300e301c37540266eb8c07cc070dd5000c4cc03cdd61807180e1baa013375c601660386ea800901a1806180d9baa0018b2032323259800980a180d9baa0018992cc004c020c070dd5000c4c8cc0200044004c080c074dd5000c5901b1804980e1baa301f301c3754003164068660126466446600400400244b30010018801c4cc080c084004cc008008c08800501f198029bac300b301c375402646018603a6ea8004cc010dd6180f180d9baa0122300b301c3754002464b30013012301c375400315980099b8f375c6040603a6ea800406a264b30013009301d37540031323300900113371e6eb8c088c07cdd50008029810980f1baa0018a5040706014603a6ea800a294101b45282036301f301c3754603e60386ea8004dd7180c800c59018180e000c5901a10140c02cdd50031bae300d300a37540066e1d20028b2010180480098021baa0098a4d13656400801", + "hash": "30268933aae8a060641fa0fb2417b675159dd2e74cc6eae65ad3b813" + }, + { + "title": "marketplace.marketplace.spend", + "datum": { + "title": "datum", + "schema": { + "$ref": "#/definitions/types~1ListingDatum" + } + }, + "redeemer": { + "title": "redeemer", + "schema": { + "$ref": "#/definitions/types~1MarketplaceRedeemer" + } + }, + "parameters": [ + { + "title": "invoice_policy_id", + "schema": { + "$ref": "#/definitions/ByteArray" + } + }, + { + "title": "escrow_script_hash", + "schema": { + "$ref": "#/definitions/ByteArray" + } + } + ], + "compiledCode": "5905290101002229800aba2aba1aba0aab9faab9eaab9dab9a9bae0039bae00248888888896600264653001300a00198051805800cdc3a4005300a0024888966002600460146ea800e26466453001159800980098069baa0028cc004c044c038dd500148c048c04cc04cc04cc04cc04cc04cc04c00646024602660266026602660266026602660260032232330010010032259800800c52845660026006602a00314a313300200230160014040809a4602460266026003222323322330020020012259800800c00e2646644b30013372200e00515980099b8f0070028800c01901544cc014014c06c0110151bae3014001375a602a002602e00280a8c8c8cc004004018896600200300389919912cc004cdc8804801456600266e3c02400a20030064059133005005301c00440586eb8c054004dd5980b000980c000a02c14bd6f7b6300a40012301230130019baf4c103d8798000488888888c9660026014602c6ea80422b300130023008301737540131598009991198041bac30073019375401e464b3001300e301a375400315980099b8f375c603c60366ea8004012266e2400e60026eacc01cc06cdd50015220100a44100402114a080ca2941019180e980d1baa301d301a37540026eb8c00cc05cdd50049bad300530173754013132598009805980b9baa0018992cc006600201f0169bae301c30193754017001400d15980099198049bac3008301a3754020464b30013013301b375400315980099b8f375c603e60386ea8004062264b30013370e9002180e1baa001899192cc004c048c078dd500144c8c8c8c8c8ca60026050003375c605000d375c605000b375a6050009375a6050004911112cc004c0b801a2646644b30013020002899192cc004c0cc00a0071640c06eb8c0c4004c0b4dd5001c566002604800515980098169baa003800c5902e45902b2056302a3754002264b3001301f0018acc004c0b0dd5003c03a2c816a2b300130230018acc004c0b0dd5003c03a2c816a2b30013370e9002000c56600260586ea801e01d1640b51640a8815102a18151baa006302d0088b205618140009813800981300098128009812000980f9baa0028b203a15980099b8f375c6042603c6ea80040162b30013375e6042604460446044603c6ea8004cdd2a4000660406ea40192f5c113009302130223022302230223022301e375400314a080e2294101c1810180e9baa0018a50406c601460386ea800a294101a45282034301e301b3754603c60366ea8004dd7180e180c9baa00b8a518b202e8b202e375c603660306ea80062c80b0c8cc004004dd61804180c1baa00e2259800800c5300103d87a80008992cc006600266e3c004dd71803180d1baa00ca50a51406113374a90001980e1ba90014bd7044cc00c00cc0780090181bae301c001406916405516405515980098011804180b9baa0098acc004c8cc88cc008008004896600200314a115980099b8f375c603a00200714a3133002002301e001406080d8dd61804180c1baa00e375c6006602e6ea80262b30019800806c0526eb8c068c05cdd5004cdd71801980b9baa009400514a316405516405516405480a88888cc024dd61804180d1baa004232598009807980d9baa0018acc004cdc79bae301f301c37540020071301398009bab3008301c375400500580220128a50406914a080d0c078c06cdd5180f180d9baa00145900c4c034dd5003cc04400d222598009802001456600260226ea802a0071640491598009804001456600260226ea802a00716404916403c8078601e60200026e1d2000300b3754007164024300a00130053754015149a26cac80181", + "hash": "2f18f39c41ea3cfa94d3e0cbbc0021d53b0ed0ae7392e428e00ad402" + }, + { + "title": "marketplace.marketplace.else", + "redeemer": { + "schema": {} + }, + "parameters": [ + { + "title": "invoice_policy_id", + "schema": { + "$ref": "#/definitions/ByteArray" + } + }, + { + "title": "escrow_script_hash", + "schema": { + "$ref": "#/definitions/ByteArray" + } + } + ], + "compiledCode": "5905290101002229800aba2aba1aba0aab9faab9eaab9dab9a9bae0039bae00248888888896600264653001300a00198051805800cdc3a4005300a0024888966002600460146ea800e26466453001159800980098069baa0028cc004c044c038dd500148c048c04cc04cc04cc04cc04cc04cc04c00646024602660266026602660266026602660260032232330010010032259800800c52845660026006602a00314a313300200230160014040809a4602460266026003222323322330020020012259800800c00e2646644b30013372200e00515980099b8f0070028800c01901544cc014014c06c0110151bae3014001375a602a002602e00280a8c8c8cc004004018896600200300389919912cc004cdc8804801456600266e3c02400a20030064059133005005301c00440586eb8c054004dd5980b000980c000a02c14bd6f7b6300a40012301230130019baf4c103d8798000488888888c9660026014602c6ea80422b300130023008301737540131598009991198041bac30073019375401e464b3001300e301a375400315980099b8f375c603c60366ea8004012266e2400e60026eacc01cc06cdd50015220100a44100402114a080ca2941019180e980d1baa301d301a37540026eb8c00cc05cdd50049bad300530173754013132598009805980b9baa0018992cc006600201f0169bae301c30193754017001400d15980099198049bac3008301a3754020464b30013013301b375400315980099b8f375c603e60386ea8004062264b30013370e9002180e1baa001899192cc004c048c078dd500144c8c8c8c8c8ca60026050003375c605000d375c605000b375a6050009375a6050004911112cc004c0b801a2646644b30013020002899192cc004c0cc00a0071640c06eb8c0c4004c0b4dd5001c566002604800515980098169baa003800c5902e45902b2056302a3754002264b3001301f0018acc004c0b0dd5003c03a2c816a2b300130230018acc004c0b0dd5003c03a2c816a2b30013370e9002000c56600260586ea801e01d1640b51640a8815102a18151baa006302d0088b205618140009813800981300098128009812000980f9baa0028b203a15980099b8f375c6042603c6ea80040162b30013375e6042604460446044603c6ea8004cdd2a4000660406ea40192f5c113009302130223022302230223022301e375400314a080e2294101c1810180e9baa0018a50406c601460386ea800a294101a45282034301e301b3754603c60366ea8004dd7180e180c9baa00b8a518b202e8b202e375c603660306ea80062c80b0c8cc004004dd61804180c1baa00e2259800800c5300103d87a80008992cc006600266e3c004dd71803180d1baa00ca50a51406113374a90001980e1ba90014bd7044cc00c00cc0780090181bae301c001406916405516405515980098011804180b9baa0098acc004c8cc88cc008008004896600200314a115980099b8f375c603a00200714a3133002002301e001406080d8dd61804180c1baa00e375c6006602e6ea80262b30019800806c0526eb8c068c05cdd5004cdd71801980b9baa009400514a316405516405516405480a88888cc024dd61804180d1baa004232598009807980d9baa0018acc004cdc79bae301f301c37540020071301398009bab3008301c375400500580220128a50406914a080d0c078c06cdd5180f180d9baa00145900c4c034dd5003cc04400d222598009802001456600260226ea802a0071640491598009804001456600260226ea802a00716404916403c8078601e60200026e1d2000300b3754007164024300a00130053754015149a26cac80181", + "hash": "2f18f39c41ea3cfa94d3e0cbbc0021d53b0ed0ae7392e428e00ad402" + } + ], + "definitions": { + "ByteArray": { + "dataType": "bytes" + }, + "Int": { + "dataType": "integer" + }, + "Option": { + "title": "Option", + "anyOf": [ + { + "title": "Some", + "description": "An optional value.", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "$ref": "#/definitions/aiken~1crypto~1VerificationKeyHash" + } + ] + }, + { + "title": "None", + "description": "Nothing.", + "dataType": "constructor", + "index": 1, + "fields": [] + } + ] + }, + "aiken/crypto/VerificationKeyHash": { + "title": "VerificationKeyHash", + "dataType": "bytes" + }, + "types/EscrowDatum": { + "title": "EscrowDatum", + "anyOf": [ + { + "title": "EscrowDatum", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "invoice_id", + "$ref": "#/definitions/ByteArray" + }, + { + "title": "seller", + "$ref": "#/definitions/aiken~1crypto~1VerificationKeyHash" + }, + { + "title": "collateral_lovelace", + "$ref": "#/definitions/Int" + }, + { + "title": "buyer", + "$ref": "#/definitions/Option" + }, + { + "title": "due_date_posix_ms", + "$ref": "#/definitions/Int" + }, + { + "title": "status", + "$ref": "#/definitions/types~1EscrowStatus" + } + ] + } + ] + }, + "types/EscrowRedeemer": { + "title": "EscrowRedeemer", + "anyOf": [ + { + "title": "Release", + "dataType": "constructor", + "index": 0, + "fields": [] + }, + { + "title": "Forfeit", + "dataType": "constructor", + "index": 1, + "fields": [] + } + ] + }, + "types/EscrowStatus": { + "title": "EscrowStatus", + "anyOf": [ + { + "title": "Locked", + "dataType": "constructor", + "index": 0, + "fields": [] + }, + { + "title": "Released", + "dataType": "constructor", + "index": 1, + "fields": [] + }, + { + "title": "Forfeited", + "dataType": "constructor", + "index": 2, + "fields": [] + } + ] + }, + "types/InvoiceMintRedeemer": { + "title": "InvoiceMintRedeemer", + "anyOf": [ + { + "title": "MintInvoice", + "dataType": "constructor", + "index": 0, + "fields": [] + }, + { + "title": "BurnInvoice", + "dataType": "constructor", + "index": 1, + "fields": [] + } + ] + }, + "types/ListingDatum": { + "title": "ListingDatum", + "anyOf": [ + { + "title": "ListingDatum", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "invoice_id", + "$ref": "#/definitions/ByteArray" + }, + { + "title": "seller", + "$ref": "#/definitions/aiken~1crypto~1VerificationKeyHash" + }, + { + "title": "asking_price_lovelace", + "$ref": "#/definitions/Int" + }, + { + "title": "original_amount_ars", + "$ref": "#/definitions/Int" + }, + { + "title": "pyth_price_at_listing", + "$ref": "#/definitions/Int" + }, + { + "title": "pyth_exponent_at_listing", + "$ref": "#/definitions/Int" + }, + { + "title": "due_date_posix_ms", + "$ref": "#/definitions/Int" + }, + { + "title": "status", + "$ref": "#/definitions/types~1ListingStatus" + } + ] + } + ] + }, + "types/ListingStatus": { + "title": "ListingStatus", + "anyOf": [ + { + "title": "Listed", + "dataType": "constructor", + "index": 0, + "fields": [] + }, + { + "title": "Sold", + "dataType": "constructor", + "index": 1, + "fields": [] + }, + { + "title": "Delisted", + "dataType": "constructor", + "index": 2, + "fields": [] + } + ] + }, + "types/MarketplaceRedeemer": { + "title": "MarketplaceRedeemer", + "anyOf": [ + { + "title": "Purchase", + "dataType": "constructor", + "index": 0, + "fields": [] + }, + { + "title": "Delist", + "dataType": "constructor", + "index": 1, + "fields": [] + } + ] + } + } +} \ No newline at end of file diff --git a/lazer/cardano/factura-ya/frontend/src/App.tsx b/lazer/cardano/factura-ya/frontend/src/App.tsx index a137d9d9..90506cac 100644 --- a/lazer/cardano/factura-ya/frontend/src/App.tsx +++ b/lazer/cardano/factura-ya/frontend/src/App.tsx @@ -3,9 +3,11 @@ import { Marketplace } from "./components/Marketplace.tsx"; import { RegisterInvoice } from "./components/RegisterInvoice.tsx"; import { PriceDisplay } from "./components/PriceDisplay.tsx"; import { WalletConnect } from "./components/WalletConnect.tsx"; +import { DemoGuide } from "./components/DemoGuide.tsx"; +import { Deploy } from "./components/Deploy.tsx"; import type { ConnectedWallet } from "./lib/wallet.ts"; -type Tab = "marketplace" | "register"; +type Tab = "marketplace" | "register" | "deploy"; export function App() { const [tab, setTab] = useState("marketplace"); @@ -39,11 +41,21 @@ export function App() { > Register Invoice + + + +
{tab === "marketplace" && } {tab === "register" && } + {tab === "deploy" && }