Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions lazer/cardano/safequote-veltrix/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Node / Next
node_modules/
.pnpm-store/
.next/
out/
dist/
build/
coverage/

# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
*.log

# Env files
.env
.env.*
!.env.example

# OS / editor
.DS_Store
Thumbs.db
.vscode/
.idea/

# TypeScript
*.tsbuildinfo

# Python
__pycache__/
*.pyc

# Aiken / generated artifacts
artifacts/
build/packages/
tmp/

# Cardano / local secrets
*.skey
*.vkey
*.addr
*.seed
wallet.json
*.mnemonic

# Test outputs
playwright-report/
test-results/

# Keep source, ignore local dependency installs inside subprojects
safequote-app-website/node_modules/
safequote-app-website/.next/
safequote-app-website/.env.local
safequote-app-website/.env.development.local
safequote-app-website/.env.production.local

safequote-app-aiken/build/packages/
68 changes: 68 additions & 0 deletions lazer/cardano/safequote-veltrix/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# SafeQuote

**Team:** Veltrix
**Member:** Elio Esis
**Contact:** eesis.dev@gmail.com

## Overview

SafeQuote is a Cardano preprod web app that lets a seller issue an invoice in USD and settle it in ADA while preserving the USD value with on-chain Pyth validation.

The flow is wallet-only. A browser wallet connects to the app, creates an invoice, and mints an invoice NFT that represents that invoice. Another wallet can pay the invoice by providing the correct PIN and sending enough ADA to satisfy the USD amount according to the current ADA/USD price verified on-chain.

## How the app works

1. A seller connects a CIP-30 browser wallet on Cardano preprod.
2. The seller creates an invoice denominated in USD.
3. The app generates a PIN and stores its hash in the invoice datum.
4. An invoice NFT is minted to represent the invoice.
5. A buyer connects a wallet and opens the invoice.
6. The buyer enters the PIN and attempts to pay in ADA.
7. The transaction uses the SafeQuote Aiken validator to verify:
- the Pyth update is valid
- the feed used is ADA/USD
- the ADA sent is at least the minimum required for the USD amount
- the PIN matches the stored hash
- the invoice NFT is transferred to the buyer
- the ADA settlement goes to the seller
8. If all checks pass, the transaction succeeds on-chain.

## How Pyth is used

SafeQuote uses Pyth as the source of truth for the ADA/USD price.

### Off-chain

The web app calls the Pyth Lazer Cardano flow to fetch the latest ADA/USD update and prepares the binary payload required for transaction building.

- Pyth product: Pyth Lazer / Cardano consumer flow
- Feed used: ADA/USD
- Cardano feed id: `16`
- Preprod policy id: `d799d287105dea9377cdf9ea8502a83d2b9eb2d2050a8aea800a21e6`

### On-chain

The Aiken validator consumes the Pyth update on-chain and verifies the ADA/USD quote before accepting payment. This ensures the invoice value is enforced in USD while settlement still happens in ADA.

## Stack

- Cardano preprod
- Aiken
- Next.js
- TypeScript
- MeshJS
- Pyth Lazer Cardano integration

## Current status

This project is being built as a hackathon MVP focused on:
- wallet-only authentication
- USD invoices settled in ADA
- invoice NFT representation
- PIN-protected payment flow
- on-chain Pyth verification with Aiken

## Notes

- This project targets Cardano preprod.
- Pyth API keys are kept in private environment variables and are not committed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Continuous Integration

on:
push:
branches: ["main"]
pull_request:

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: aiken-lang/setup-aiken@v1
with:
version: v1.1.19
- run: aiken fmt --check
- run: aiken check -D
- run: aiken build
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Aiken compilation artifacts
artifacts/
# Aiken's project working directory
build/
# Aiken's default documentation export
docs/
65 changes: 65 additions & 0 deletions lazer/cardano/safequote-veltrix/safequote-app-aiken/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# safequote_app_aiken

Write validators in the `validators` folder, and supporting functions in the `lib` folder using `.ak` as a file extension.

```aiken
validator my_first_validator {
spend(_datum: Option<Data>, _redeemer: Data, _output_reference: Data, _context: Data) {
True
}
}
```

## Building

```sh
aiken build
```

## Configuring

**aiken.toml**
```toml
[config.default]
network_id = 41
```

Or, alternatively, write conditional environment modules under `env`.

## Testing

You can write tests in any module using the `test` keyword. For example:

```aiken
use config

test foo() {
config.network_id + 1 == 42
}
```

To run all tests, simply do:

```sh
aiken check
```

To run only tests matching the string `foo`, do:

```sh
aiken check -m foo
```

## Documentation

If you're writing a library, you might want to generate an HTML documentation for it.

Use:

```sh
aiken docs
```

## Resources

Find more on the [Aiken's user manual](https://aiken-lang.org).
27 changes: 27 additions & 0 deletions lazer/cardano/safequote-veltrix/safequote-app-aiken/aiken.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# This file was generated by Aiken
# You typically do not need to edit this file

[[requirements]]
name = "aiken-lang/stdlib"
version = "v3.0.0"
source = "github"

[[requirements]]
name = "pyth-network/pyth-lazer-cardano"
version = "main"
source = "github"

[[packages]]
name = "aiken-lang/stdlib"
version = "v3.0.0"
requirements = []
source = "github"

[[packages]]
name = "pyth-network/pyth-lazer-cardano"
version = "main"
requirements = []
source = "github"

[etags]
"pyth-network/pyth-lazer-cardano@main" = [{ secs_since_epoch = 1774214111, nanos_since_epoch = 474416700 }, "a46dacd97a22eb07feeaf966d48c3116c8249ddc836705656e3135cea285bcfc"]
23 changes: 23 additions & 0 deletions lazer/cardano/safequote-veltrix/safequote-app-aiken/aiken.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name = "esis8/safequote_app_aiken"
version = "0.0.0"
compiler = "v1.1.19"
plutus = "v3"
license = "Apache-2.0"
description = "SafeQuote Aiken contracts"

[repository]
user = "esis8"
project = "safequote_app_aiken"
platform = "github"

[[dependencies]]
name = "aiken-lang/stdlib"
version = "v3.0.0"
source = "github"

[[dependencies]]
name = "pyth-network/pyth-lazer-cardano"
version = "main"
source = "github"

[config]
181 changes: 181 additions & 0 deletions lazer/cardano/safequote-veltrix/safequote-app-aiken/plutus.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
use validators/invoice

test minimum_lovelace_required_handles_negative_exponent() {
invoice.minimum_lovelace_required(100, 250, -2) == 400_000
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
use aiken/collection/list
use aiken/crypto.{VerificationKeyHash, blake2b_256}
use aiken/math
use cardano/address
use cardano/assets.{AssetName, PolicyId}
use cardano/transaction.{OutputReference, Transaction}
use pyth
use types/u32

pub const ada_usd_feed_id = 16

pub type InvoiceDatum {
InvoiceDatum {
seller: VerificationKeyHash,
usd_amount_cents: Int,
pyth_policy_id: PolicyId,
pin_hash: ByteArray,
invoice_nft_policy_id: PolicyId,
invoice_nft_name: AssetName,
deadline: Int,
}
}

pub type InvoiceRedeemer {
Pay { buyer: VerificationKeyHash, pin: ByteArray }
}

pub fn minimum_lovelace_required(
usd_amount_cents: Int,
ada_usd_price: Int,
exponent: Int,
) -> Int {
let scale = math.pow(10, 0 - exponent)
let numerator = usd_amount_cents * 1_000_000 * scale
let denominator = ada_usd_price * 100
( numerator + denominator - 1 ) / denominator
}

pub fn read_ada_usd_quote(
pyth_policy_id: PolicyId,
tx: Transaction,
) -> (Int, Int) {
expect [update] = pyth.get_updates(pyth_policy_id, tx)

expect Some(feed) =
list.find(
update.feeds,
fn(feed) { u32.as_int(feed.feed_id) == ada_usd_feed_id },
)

expect Some(Some(price)) = feed.price
expect Some(exponent) = feed.exponent
(price, exponent)
}

pub fn pays_seller_at_least(
seller: VerificationKeyHash,
minimum_lovelace: Int,
tx: Transaction,
) -> Bool {
let seller_address = address.from_verification_key(seller)

list.any(
tx.outputs,
fn(output) {
output.address == seller_address && assets.lovelace_of(output.value) >= minimum_lovelace
},
)
}

pub fn sends_invoice_nft_to_buyer(
buyer: VerificationKeyHash,
invoice_nft_policy_id: PolicyId,
invoice_nft_name: AssetName,
tx: Transaction,
) -> Bool {
let buyer_address = address.from_verification_key(buyer)

list.any(
tx.outputs,
fn(output) {
output.address == buyer_address && assets.has_nft(
output.value,
invoice_nft_policy_id,
invoice_nft_name,
)
},
)
}

validator invoice {
spend(
datum: Option<InvoiceDatum>,
redeemer: InvoiceRedeemer,
_own_ref: OutputReference,
tx: Transaction,
) {
expect Some(datum) = datum
let Pay { buyer, pin } = redeemer

let (price, exponent) = read_ada_usd_quote(datum.pyth_policy_id, tx)
let min_lovelace =
minimum_lovelace_required(datum.usd_amount_cents, price, exponent)

and {
blake2b_256(pin) == datum.pin_hash,
pays_seller_at_least(datum.seller, min_lovelace, tx),
sends_invoice_nft_to_buyer(
buyer,
datum.invoice_nft_policy_id,
datum.invoice_nft_name,
tx,
),
}
}

else(_) {
fail
}
}
Loading