diff --git a/README.md b/README.md
index 4ff7453..be09fc6 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
> Order on Starknet, write on Bitcoin, get money trustlessly, repeat
-Broly is a decentralized Bitcoin inscription service that uses Starknet for orderbook management and escrow. It enables trustless Bitcoin inscriptions with guaranteed payments through smart contracts.
+Broly is a decentralized Bitcoin inscription service that uses Starknet for orderbook management. It enables trustless Bitcoin inscriptions with guaranteed payments through smart contracts.
@@ -34,7 +34,6 @@ flowchart TB
subgraph Starknet
OB[Orderbook Contract]
- ES[Escrow Contract]
end
subgraph Bitcoin
@@ -51,10 +50,8 @@ flowchart TB
UI <--> SW
API --> DB
SW <--> OB
- SW <--> ES
IS --> BTC
OM --> OB
- OM --> ES
API --> IS
IS --> API
```
@@ -65,11 +62,11 @@ flowchart TB
2. User creates an inscription order:
- Specifies inscription content and reward amount
- Order is created on Starknet orderbook
- - Funds are locked in escrow contract
+ - Funds are locked in the contract
3. Inscribor service:
- Monitors pending orders
- Creates Bitcoin inscriptions
- - Triggers escrow release on successful inscription
+ - Triggers reward release on successful inscription
4. User receives inscription, inscribor receives reward
## Getting Started
@@ -145,7 +142,6 @@ broly/
### Smart Contracts (onchain)
- Orderbook contract
-- Escrow contract
- Payment handling
### Inscribor Service
@@ -153,7 +149,7 @@ broly/
- Order monitoring
- Bitcoin inscription creation
- Transaction verification
-- Starknet interaction for escrow release
+- Starknet interaction for reward release
## License
diff --git a/apps/web/src/components/Header.tsx b/apps/web/src/components/Header.tsx
index 50a91e0..f65273c 100644
--- a/apps/web/src/components/Header.tsx
+++ b/apps/web/src/components/Header.tsx
@@ -1,4 +1,5 @@
import { NavLink } from "react-router";
+import { useAccount } from "@starknet-react/core";
import "./Header.css";
function Header(props: any) {
diff --git a/packages/onchain/Scarb.lock b/packages/onchain/Scarb.lock
index ac734f9..aee286b 100644
--- a/packages/onchain/Scarb.lock
+++ b/packages/onchain/Scarb.lock
@@ -6,15 +6,27 @@ name = "alexandria_math"
version = "0.2.1"
source = "git+https://github.com/keep-starknet-strange/alexandria#95d98a5182001d07673b856a356eff0e6bd05354"
+[[package]]
+name = "consensus"
+version = "0.1.0"
+source = "git+https://github.com/keep-starknet-strange/raito.git?rev=02a13045b7074ae2b3247431cd91f1ad76263fb2#02a13045b7074ae2b3247431cd91f1ad76263fb2"
+dependencies = [
+ "shinigami_engine",
+ "utils",
+]
+
[[package]]
name = "onchain"
version = "0.1.0"
dependencies = [
"alexandria_math",
+ "consensus",
"openzeppelin",
"openzeppelin_token",
"openzeppelin_utils",
"snforge_std",
+ "utils",
+ "utu_relay",
]
[[package]]
@@ -123,6 +135,31 @@ name = "openzeppelin_utils"
version = "0.19.0"
source = "git+https://github.com/openzeppelin/cairo-contracts?tag=v0.19.0#8d49e8c445efd9bdc99b050c8b7d11ae5ad19628"
+[[package]]
+name = "ripemd160"
+version = "0.1.0"
+source = "git+https://github.com/j1mbo64/ripemd160_cairo.git#833e07d7d074d4ee51ceb40a5bcb4af2fe6898f3"
+
+[[package]]
+name = "sha1"
+version = "0.1.0"
+source = "git+https://github.com/j1mbo64/sha1_cairo.git#2b65bc00a829bdcc244c140d0f31feda32f8d2c4"
+
+[[package]]
+name = "shinigami_engine"
+version = "0.1.0"
+source = "git+https://github.com/keep-starknet-strange/shinigami.git?rev=3415ed6#3415ed6331d3ea2dc2de6f9ab8e0be6562585f2d"
+dependencies = [
+ "ripemd160",
+ "sha1",
+ "shinigami_utils",
+]
+
+[[package]]
+name = "shinigami_utils"
+version = "0.1.0"
+source = "git+https://github.com/keep-starknet-strange/shinigami.git?rev=3415ed6#3415ed6331d3ea2dc2de6f9ab8e0be6562585f2d"
+
[[package]]
name = "snforge_scarb_plugin"
version = "0.33.0"
@@ -135,3 +172,18 @@ source = "git+https://github.com/foundry-rs/starknet-foundry?tag=v0.33.0#221b1db
dependencies = [
"snforge_scarb_plugin",
]
+
+[[package]]
+name = "utils"
+version = "0.1.0"
+source = "git+https://github.com/keep-starknet-strange/raito.git?rev=02a13045b7074ae2b3247431cd91f1ad76263fb2#02a13045b7074ae2b3247431cd91f1ad76263fb2"
+
+[[package]]
+name = "utu_relay"
+version = "0.1.0"
+source = "git+https://github.com/lana-shanghai/utu_relay.git#25f8d9799df04465c716155e9ece61d9145b0f8c"
+dependencies = [
+ "openzeppelin",
+ "openzeppelin_upgrades",
+ "utils",
+]
diff --git a/packages/onchain/Scarb.toml b/packages/onchain/Scarb.toml
index 4a81186..dc3e2ff 100644
--- a/packages/onchain/Scarb.toml
+++ b/packages/onchain/Scarb.toml
@@ -5,6 +5,9 @@ edition = "2024_07"
# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html
+[patch.crates-io]
+openzeppelin = "0.19.0"
+
[dependencies]
snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.33.0" }
openzeppelin = { git = "https://github.com/openzeppelin/cairo-contracts", tag = "v0.19.0" }
@@ -12,11 +15,19 @@ starknet = "2.9.1"
alexandria_math = { git = "https://github.com/keep-starknet-strange/alexandria" }
openzeppelin_token = { git = "https://github.com/openzeppelin/cairo-contracts", tag = "v0.19.0" }
openzeppelin_utils = { git = "https://github.com/openzeppelin/cairo-contracts", tag = "v0.19.0" }
+utils = { git = "https://github.com/keep-starknet-strange/raito.git", rev = "02a13045b7074ae2b3247431cd91f1ad76263fb2" }
+consensus = { git = "https://github.com/keep-starknet-strange/raito.git", rev = "02a13045b7074ae2b3247431cd91f1ad76263fb2" }
+utu_relay = { git = "https://github.com/lana-shanghai/utu_relay.git" }
[[target.starknet-contract]]
casm = true
sierra = true
-build-external-contracts = ["openzeppelin_presets::erc20::ERC20Upgradeable"]
+build-external-contracts = [
+ "openzeppelin_presets::erc20::ERC20Upgradeable",
+ "utu_relay::utu_relay::UtuRelay"
+]
+allowed-libfuncs-list.name = "experimental"
+casm-add-pythonic-hints = true
[dev-dependencies]
assert_macros = "2.9.1"
diff --git a/packages/onchain/src/escrow.cairo b/packages/onchain/src/escrow.cairo
deleted file mode 100644
index 2af7051..0000000
--- a/packages/onchain/src/escrow.cairo
+++ /dev/null
@@ -1 +0,0 @@
-mod escrow;
diff --git a/packages/onchain/src/escrow/escrow.cairo b/packages/onchain/src/escrow/escrow.cairo
deleted file mode 100644
index b69d849..0000000
--- a/packages/onchain/src/escrow/escrow.cairo
+++ /dev/null
@@ -1,17 +0,0 @@
-#[starknet::interface]
-trait IEscrow
{
- fn greet(ref self: TContractState) -> felt252;
-}
-
-#[starknet::contract]
-mod Escrow {
- #[storage]
- struct Storage {}
-
- #[abi(embed_v0)]
- impl EscrowImpl of super::IEscrow {
- fn greet(ref self: ContractState) -> felt252 {
- 'Kakarotto'
- }
- }
-}
diff --git a/packages/onchain/src/lib.cairo b/packages/onchain/src/lib.cairo
index 9bfc256..51b511c 100644
--- a/packages/onchain/src/lib.cairo
+++ b/packages/onchain/src/lib.cairo
@@ -1,3 +1,3 @@
-mod escrow;
mod orderbook;
mod utils;
+mod relay;
diff --git a/packages/onchain/src/orderbook/interface.cairo b/packages/onchain/src/orderbook/interface.cairo
index 43f720d..aa99638 100644
--- a/packages/onchain/src/orderbook/interface.cairo
+++ b/packages/onchain/src/orderbook/interface.cairo
@@ -16,16 +16,15 @@ pub trait IOrderbook {
ref self: TContractState,
inscription_data: ByteArray,
receiving_address: ByteArray,
- satoshi: felt252,
currency_fee: felt252,
submitter_fee: u256,
) -> u32;
fn cancel_inscription(ref self: TContractState, inscription_id: u32, currency_fee: felt252);
fn lock_inscription(ref self: TContractState, inscription_id: u32, tx_hash: ByteArray);
fn submit_inscription(ref self: TContractState, inscription_id: u32, tx_hash: ByteArray);
- fn query_inscription(self: @TContractState, inscription_id: u32) -> (ByteArray, u256);
- fn is_valid_bitcoin_address(self: @TContractState, receiving_address: ByteArray) -> bool;
- fn is_locked(self: @TContractState, tx_hash: ByteArray) -> (bool, ContractAddress);
+ fn query_inscription(
+ self: @TContractState, inscription_id: u32,
+ ) -> (ContractAddress, ByteArray, u256);
}
#[starknet::interface]
@@ -34,16 +33,15 @@ pub trait OrderbookABI {
ref self: TContractState,
inscription_data: ByteArray,
receiving_address: ByteArray,
- satoshi: felt252,
currency_fee: felt252,
submitter_fee: u256,
) -> u32;
fn cancel_inscription(ref self: TContractState, inscription_id: u32, currency_fee: felt252);
fn lock_inscription(ref self: TContractState, inscription_id: u32, tx_hash: ByteArray);
fn submit_inscription(ref self: TContractState, inscription_id: u32, tx_hash: ByteArray);
- fn query_inscription(self: @TContractState, inscription_id: u32) -> (ByteArray, u256);
- fn is_valid_bitcoin_address(self: @TContractState, receiving_address: ByteArray) -> bool;
- fn is_locked(self: @TContractState, tx_hash: ByteArray) -> (bool, ContractAddress);
+ fn query_inscription(
+ self: @TContractState, inscription_id: u32,
+ ) -> (ContractAddress, ByteArray, u256);
// ERC20 functions
fn balance_of(self: @TContractState, account: ContractAddress) -> felt252;
diff --git a/packages/onchain/src/orderbook/mock.cairo b/packages/onchain/src/orderbook/mock.cairo
index 96cbc75..6f58bf8 100644
--- a/packages/onchain/src/orderbook/mock.cairo
+++ b/packages/onchain/src/orderbook/mock.cairo
@@ -17,7 +17,7 @@ mod OrderbookMock {
new_inscription_id: u32,
// A map from the inscription ID to a tuple with the inscribed
// data and submitter fee.
- inscriptions: Map,
+ inscriptions: Map,
// A map from the inscription ID to status. Possible values:
// 'Open', 'Locked', 'Canceled', 'Closed'.
inscription_statuses: Map,
@@ -47,7 +47,6 @@ mod OrderbookMock {
caller: ContractAddress,
inscription_data: ByteArray,
receiving_address: ByteArray,
- satoshi: felt252,
currency_fee: felt252,
submitter_fee: u256,
}
@@ -85,7 +84,6 @@ mod OrderbookMock {
/// Inputs:
/// - `inscription_data: ByteArray`, the data to be inscribed on Bitcoin.
/// - `receiving_address: ByteArray`, the taproot address that will own the inscription.
- /// - `satoshi: felt252`, the Sat where the user wants to inscribe data.
/// - `currency_fee: felt252`, 'STRK' tokens.
/// - `submitter_fee: u256`, fee to be paid to the submitter for the inscription.
/// Returns:
@@ -94,12 +92,13 @@ mod OrderbookMock {
ref self: ContractState,
inscription_data: ByteArray,
receiving_address: ByteArray,
- satoshi: felt252,
currency_fee: felt252,
submitter_fee: u256,
) -> u32 {
+ assert(currency_fee == 'STRK'.into(), 'The currency is not supported');
+ let caller = get_caller_address();
let id = self.new_inscription_id.read();
- self.inscriptions.write(id, (inscription_data.clone(), submitter_fee));
+ self.inscriptions.write(id, (caller, inscription_data.clone(), submitter_fee));
self.inscription_statuses.write(id, Status::Open);
self.new_inscription_id.write(id + 1);
self
@@ -109,7 +108,6 @@ mod OrderbookMock {
caller: get_caller_address(),
inscription_data: inscription_data,
receiving_address: receiving_address,
- satoshi: satoshi,
currency_fee: currency_fee,
submitter_fee: submitter_fee,
},
@@ -123,8 +121,24 @@ mod OrderbookMock {
/// cancel.
/// - `currency_fee: felt252`, the token that the user paid the submitter fee in.
fn cancel_inscription(ref self: ContractState, inscription_id: u32, currency_fee: felt252) {
- let (inscription_data, _) = self.inscriptions.read(inscription_id);
- self.inscriptions.write(inscription_id, (inscription_data, 0));
+ let caller = get_caller_address();
+ let (request_creator, inscription_data, amount) = self
+ .inscriptions
+ .read(inscription_id);
+ assert(caller == request_creator, 'Caller cannot cancel this id');
+
+ let status = self.inscription_statuses.read(inscription_id);
+ assert(status != Status::Undefined, 'Inscription does not exist');
+ assert(status != Status::Locked, 'The inscription is locked');
+ assert(status != Status::Canceled, 'The inscription is canceled');
+ assert(status != Status::Closed, 'The inscription has been closed');
+
+ let escrow_address = get_contract_address();
+ if (currency_fee == 'STRK'.into()) {
+ let strk_token = self.strk_token.read();
+ strk_token.transfer_from(sender: escrow_address, recipient: caller, amount: amount);
+ }
+ self.inscriptions.write(inscription_id, (caller, inscription_data, 0));
self.inscription_statuses.write(inscription_id, Status::Canceled);
self
.emit(
@@ -165,18 +179,11 @@ mod OrderbookMock {
self.emit(RequestCompleted { inscription_id: inscription_id, tx_hash: tx_hash });
}
- fn query_inscription(self: @ContractState, inscription_id: u32) -> (ByteArray, u256) {
+ fn query_inscription(
+ self: @ContractState, inscription_id: u32,
+ ) -> (ContractAddress, ByteArray, u256) {
return self.inscriptions.read(inscription_id);
}
-
-
- fn is_valid_bitcoin_address(self: @ContractState, receiving_address: ByteArray) -> bool {
- return true;
- }
-
- fn is_locked(self: @ContractState, tx_hash: ByteArray) -> (bool, ContractAddress) {
- return (true, get_contract_address());
- }
}
#[generate_trait]
diff --git a/packages/onchain/src/orderbook/orderbook.cairo b/packages/onchain/src/orderbook/orderbook.cairo
index 4bcedaf..7cfb441 100644
--- a/packages/onchain/src/orderbook/orderbook.cairo
+++ b/packages/onchain/src/orderbook/orderbook.cairo
@@ -10,14 +10,15 @@ mod Orderbook {
StoragePointerReadAccess, StoragePointerWriteAccess,
};
use starknet::{ContractAddress, get_caller_address, get_contract_address, get_block_number};
+ use starknet::{SyscallResultTrait, syscalls::call_contract_syscall};
#[storage]
struct Storage {
// ID of the next inscription.
new_inscription_id: u32,
- // A map from the inscription ID to a tuple with the inscribed
- // data and submitter fee.
- inscriptions: Map,
+ // A map from the inscription ID to a tuple with the caller, the
+ // inscribed data, and submitter fee.
+ inscriptions: Map,
// A map from the inscription ID to status. Possible values:
// 'Open', 'Locked', 'Canceled', 'Closed'.
inscription_statuses: Map,
@@ -28,12 +29,60 @@ mod Orderbook {
inscription_locks: Map,
// STRK fee token.
strk_token: ERC20ABIDispatcher,
+ // Address of the contract checking transaction inclusion.
+ relay_address: ContractAddress,
}
#[constructor]
- fn constructor(ref self: ContractState, strk_token: ContractAddress) {
+ fn constructor(
+ ref self: ContractState, strk_token: ContractAddress, relay_address: ContractAddress,
+ ) {
// initialize contract
- self.initializer(:strk_token);
+ self.initializer(:strk_token, :relay_address);
+ }
+
+ #[event]
+ #[derive(Drop, starknet::Event)]
+ pub enum Event {
+ RequestCreated: RequestCreated,
+ RequestCanceled: RequestCanceled,
+ RequestLocked: RequestLocked,
+ RequestCompleted: RequestCompleted,
+ }
+
+ #[derive(Drop, starknet::Event)]
+ pub struct RequestCreated {
+ #[key]
+ pub id: u32,
+ #[key]
+ pub caller: ContractAddress,
+ pub inscription_data: ByteArray,
+ pub receiving_address: ByteArray,
+ pub currency_fee: felt252,
+ pub submitter_fee: u256,
+ }
+
+ #[derive(Drop, starknet::Event)]
+ pub struct RequestCanceled {
+ #[key]
+ pub id: u32,
+ pub currency_fee: felt252,
+ }
+
+ #[derive(Drop, starknet::Event)]
+ pub struct RequestLocked {
+ #[key]
+ pub id: u32,
+ pub submitter: ContractAddress,
+ pub tx_hash: ByteArray,
+ }
+
+ #[derive(Drop, starknet::Event)]
+ pub struct RequestCompleted {
+ #[key]
+ pub id: u32,
+ pub submitter: ContractAddress,
+ pub tx_hash: ByteArray,
}
#[abi(embed_v0)]
@@ -42,7 +91,6 @@ mod Orderbook {
/// Inputs:
/// - `inscription_data: ByteArray`, the data to be inscribed on Bitcoin.
/// - `receiving_address: ByteArray`, the taproot address that will own the inscription.
- /// - `satoshi: felt252`, the Sat where the user wants to inscribe data.
/// - `currency_fee: felt252`, 'STRK' tokens.
/// - `submitter_fee: u256`, fee to be paid to the submitter for the inscription.
/// Returns:
@@ -51,46 +99,43 @@ mod Orderbook {
ref self: ContractState,
inscription_data: ByteArray,
receiving_address: ByteArray,
- satoshi: felt252,
currency_fee: felt252,
submitter_fee: u256,
) -> u32 {
- assert(
- self.is_valid_bitcoin_address(receiving_address) == true,
- 'Not a valid bitcoin address',
- );
assert(currency_fee == 'STRK'.into(), 'The currency is not supported');
let caller = get_caller_address();
let escrow_address = get_contract_address();
if (currency_fee == 'STRK'.into()) {
let strk_token = self.strk_token.read();
- // TODO: change the transfer to the escrow contract once it's implemented.
strk_token
.transfer_from(
sender: caller, recipient: escrow_address, amount: submitter_fee,
);
}
let id = self.new_inscription_id.read();
- self.inscriptions.write(id, (inscription_data, submitter_fee));
+ self.inscriptions.write(id, (caller, inscription_data.clone(), submitter_fee));
self.inscription_statuses.write(id, Status::Open);
+ self
+ .emit(
+ RequestCreated {
+ id: id,
+ caller: caller,
+ inscription_data: inscription_data,
+ receiving_address: receiving_address,
+ currency_fee: currency_fee,
+ submitter_fee: submitter_fee,
+ },
+ );
id
}
- /// Helper function that checks the format of the taproot address.
- /// Inputs:
- /// - `receiving_address: ByteArray`, the ID of the inscription.
- /// Returns:
- /// - `bool`
- fn is_valid_bitcoin_address(self: @ContractState, receiving_address: ByteArray) -> bool {
- // TODO: implement the check that the receiving address is in valid format.
- true
- }
-
/// Inputs:
/// - `inscription_id: felt252`, the ID of the inscription.
/// Returns:
/// - `(ByteArray, felt252)`, the tuple with the inscribed data and the fee.
- fn query_inscription(self: @ContractState, inscription_id: u32) -> (ByteArray, u256) {
+ fn query_inscription(
+ self: @ContractState, inscription_id: u32,
+ ) -> (ContractAddress, ByteArray, u256) {
self.inscriptions.read(inscription_id)
}
@@ -100,23 +145,26 @@ mod Orderbook {
/// cancel.
/// - `currency_fee: felt252`, the token that the user paid the submitter fee in.
fn cancel_inscription(ref self: ContractState, inscription_id: u32, currency_fee: felt252) {
+ let caller = get_caller_address();
+ let (request_creator, inscription_data, amount) = self
+ .inscriptions
+ .read(inscription_id);
+ assert(caller == request_creator, 'Caller cannot cancel this id');
+
let status = self.inscription_statuses.read(inscription_id);
assert(status != Status::Undefined, 'Inscription does not exist');
assert(status != Status::Locked, 'The inscription is locked');
assert(status != Status::Canceled, 'The inscription is canceled');
assert(status != Status::Closed, 'The inscription has been closed');
- let caller = get_caller_address();
- // TODO: change the address to the actual escrow contract once it's implemented.
let escrow_address = get_contract_address();
if (currency_fee == 'STRK'.into()) {
let strk_token = self.strk_token.read();
- let (_, amount) = self.inscriptions.read(inscription_id);
strk_token.transfer_from(sender: escrow_address, recipient: caller, amount: amount);
}
- let (inscription_data, _) = self.inscriptions.read(inscription_id);
- self.inscriptions.write(inscription_id, (inscription_data, 0));
+ self.inscriptions.write(inscription_id, (caller, inscription_data, 0));
self.inscription_statuses.write(inscription_id, Status::Canceled);
+ self.emit(RequestCanceled { id: inscription_id, currency_fee: currency_fee });
}
/// Called by a submitter. Multiple submitters are allowed to lock the
@@ -146,6 +194,7 @@ mod Orderbook {
submitters.write(submitter, submitter);
self.inscription_statuses.write(inscription_id, Status::Locked);
+ self.emit(RequestLocked { id: inscription_id, submitter: submitter, tx_hash: tx_hash });
}
/// Called by a submitter. The fee is transferred to the submitter if
@@ -156,25 +205,28 @@ mod Orderbook {
/// - `inscription_id: felt252`, the ID of the inscription being locked.
/// - `tx_hash: ByteArray`, the hash of the transaction submitted to Bitcoin.
fn submit_inscription(ref self: ContractState, inscription_id: u32, tx_hash: ByteArray) {
+ let caller = get_caller_address();
+ let submitters = self.submitters.entry(inscription_id);
+ let submitter = submitters.read(caller);
+ assert(caller == submitter, 'Caller does not match submitter');
+
let (_, precomputed_tx_hash, _) = self.inscription_locks.read(inscription_id);
assert(precomputed_tx_hash == tx_hash, 'Precomputed hash != submitted');
- // TODO: process the submitted transaction hash, verify that it is on Bitcoin
+ const selector: felt252 = selector!("prove_inclusion");
+ let to = self.relay_address.read();
+ let calldata: Array = array![];
- self.inscription_statuses.write(inscription_id, Status::Closed);
- }
+ // TODO: assert successful inclusion call
+ call_contract_syscall(to, selector, calldata.span()).unwrap_syscall();
- /// Helper function that checks if the inscription has already been locked.
- /// Inputs:
- /// - `tx_hash: ByteArray`, the precomputed transaction hash for the inscription
- /// being locked.
- /// Returns:
- /// - `(bool, ContractAddress)`
- fn is_locked(self: @ContractState, tx_hash: ByteArray) -> (bool, ContractAddress) {
- // TODO: fetch the relevant lock made with the precomputed tx hash
+ // TODO: assert that the witness data contains the requested inscription
- let caller = get_caller_address();
- (true, caller)
+ self.inscription_statuses.write(inscription_id, Status::Closed);
+ self
+ .emit(
+ RequestCompleted { id: inscription_id, submitter: submitter, tx_hash: tx_hash },
+ );
}
}
@@ -182,8 +234,11 @@ mod Orderbook {
pub impl InternalImpl of InternalTrait {
/// Executed once when the Orderbook contract is deployed. Used to set
/// initial values for contract storage variables for the fee tokens.
- fn initializer(ref self: ContractState, strk_token: ContractAddress) {
+ fn initializer(
+ ref self: ContractState, strk_token: ContractAddress, relay_address: ContractAddress,
+ ) {
self.strk_token.write(ERC20ABIDispatcher { contract_address: strk_token });
+ self.relay_address.write(relay_address);
}
}
}
diff --git a/packages/onchain/src/orderbook/test_orderbook.cairo b/packages/onchain/src/orderbook/test_orderbook.cairo
index 050bd78..5bb5bdb 100644
--- a/packages/onchain/src/orderbook/test_orderbook.cairo
+++ b/packages/onchain/src/orderbook/test_orderbook.cairo
@@ -13,50 +13,66 @@ use onchain::utils::{constants, erc20_utils};
fn setup_orderbook(
- erc20_contract_address: ContractAddress,
-) -> (OrderbookABIDispatcher, ContractAddress) {
+ erc20_contract_address: ContractAddress, relay_address: ContractAddress,
+) -> OrderbookABIDispatcher {
// declare Orderbook contract
let contract_class = declare("Orderbook").unwrap().contract_class();
// deploy Orderbook contract
let mut calldata = array![];
calldata.append_serde(erc20_contract_address);
+ calldata.append_serde(relay_address);
let (contract_address, _) = contract_class.deploy(@calldata).unwrap();
- (OrderbookABIDispatcher { contract_address }, contract_address)
+ OrderbookABIDispatcher { contract_address }
}
-fn setup() -> (
- OrderbookABIDispatcher, ContractAddress, ERC20UpgradeableABIDispatcher, ContractAddress,
-) {
+fn setup_relay() -> ContractAddress {
+ // declare TransactionInclusion contract
+ let contract_class = declare("TransactionInclusion").unwrap().contract_class();
+
+ // deploy TransactionInclusion contract
+ let mut calldata = array![];
+ calldata.append_serde(constants::UTU()); // TODO replace with deployed Utu contract
+
+ let (relay_address, _) = contract_class.deploy(@calldata).unwrap();
+
+ relay_address
+}
+
+fn setup() -> (OrderbookABIDispatcher, ERC20UpgradeableABIDispatcher) {
// deploy an ERC20
- let (erc20_strk, erc20_address) = erc20_utils::setup_erc20(test_address());
+ let (erc20_strk, _) = erc20_utils::setup_erc20(test_address());
+
+ // deploy relay contract
+ let relay_address = setup_relay();
// deploy Orderbook contract
- let (orderbook, contract_address) = setup_orderbook(erc20_strk.contract_address);
+ let orderbook = setup_orderbook(erc20_strk.contract_address, relay_address);
- (orderbook, contract_address, erc20_strk, erc20_address)
+ (orderbook, erc20_strk)
}
#[test]
fn test_request_inscription_stored_and_retrieved() {
- let (orderbook_dispatcher, contract_address, token_dispatcher, _) = setup();
+ let (orderbook_dispatcher, token_dispatcher) = setup();
let test_taproot_address: ByteArray =
"bc1p5d7rjq7g6r4jdyhzks9smlaqtedr4dekq08ge8ztwac72sfr9rusxg3297";
let test_data: ByteArray = "data";
- token_dispatcher.approve(contract_address, 100);
+ token_dispatcher.approve(orderbook_dispatcher.contract_address, 100);
- orderbook_dispatcher.request_inscription(test_data, test_taproot_address, 1, 'STRK'.into(), 10);
+ orderbook_dispatcher.request_inscription(test_data, test_taproot_address, 'STRK'.into(), 10);
- let expected = ("data", 10); // the inscription data and the submitter fee
+ let expected = (test_address(), "data", 10); // the inscription data and the submitter fee
let actual = orderbook_dispatcher.query_inscription(0);
assert_eq!(expected, actual);
let expected_contract_balance = 10; // the submitter fee transferred to the contract
- let actual_contract_balance = token_dispatcher.balance_of(contract_address);
+ let actual_contract_balance = token_dispatcher
+ .balance_of(orderbook_dispatcher.contract_address);
assert_eq!(expected_contract_balance, actual_contract_balance);
let expected_user_balance = constants::SUPPLY - 10; // the user balance after the request call
@@ -67,41 +83,40 @@ fn test_request_inscription_stored_and_retrieved() {
#[test]
#[should_panic]
fn test_request_inscription_fails_wrong_currency() {
- let (orderbook_dispatcher, contract_address, token_dispatcher, _) = setup();
+ let (orderbook_dispatcher, token_dispatcher) = setup();
let test_taproot_address: ByteArray = "test";
let test_data: ByteArray = "data";
- token_dispatcher.approve(contract_address, 100);
+ token_dispatcher.approve(orderbook_dispatcher.contract_address, 100);
- orderbook_dispatcher.request_inscription(test_data, test_taproot_address, 1, 'BTC'.into(), 10);
+ orderbook_dispatcher.request_inscription(test_data, test_taproot_address, 'BTC'.into(), 10);
}
#[test]
#[should_panic]
fn test_request_inscription_fails_insufficient_balance() {
- let (orderbook_dispatcher, contract_address, token_dispatcher, _) = setup();
+ let (orderbook_dispatcher, token_dispatcher) = setup();
let test_taproot_address: ByteArray = "test";
let test_data: ByteArray = "data";
- token_dispatcher.approve(contract_address, 2000);
+ token_dispatcher.approve(orderbook_dispatcher.contract_address, 2000);
- orderbook_dispatcher
- .request_inscription(test_data, test_taproot_address, 1, 'STRK'.into(), 2000);
+ orderbook_dispatcher.request_inscription(test_data, test_taproot_address, 'STRK'.into(), 2000);
}
#[test]
fn test_lock_inscription_works() {
- let (orderbook_dispatcher, contract_address, token_dispatcher, _) = setup();
+ let (orderbook_dispatcher, token_dispatcher) = setup();
let test_taproot_address: ByteArray = "test";
let test_data: ByteArray = "data";
- token_dispatcher.approve(contract_address, 100);
+ token_dispatcher.approve(orderbook_dispatcher.contract_address, 100);
let id = orderbook_dispatcher
- .request_inscription(test_data, test_taproot_address, 1, 'STRK'.into(), 10);
+ .request_inscription(test_data, test_taproot_address, 'STRK'.into(), 10);
start_cheat_block_number_global(1000);
orderbook_dispatcher.lock_inscription(id, "hash");
@@ -111,15 +126,15 @@ fn test_lock_inscription_works() {
#[test]
#[should_panic]
fn test_lock_inscription_fails_prior_lock_not_expired() {
- let (orderbook_dispatcher, contract_address, token_dispatcher, _) = setup();
+ let (orderbook_dispatcher, token_dispatcher) = setup();
let test_taproot_address: ByteArray = "test";
let test_data: ByteArray = "data";
- token_dispatcher.approve(contract_address, 100);
+ token_dispatcher.approve(orderbook_dispatcher.contract_address, 100);
let id = orderbook_dispatcher
- .request_inscription(test_data, test_taproot_address, 1, 'STRK'.into(), 10);
+ .request_inscription(test_data, test_taproot_address, 'STRK'.into(), 10);
orderbook_dispatcher.lock_inscription(id, "hash");
orderbook_dispatcher.lock_inscription(id, "other_hash");
@@ -128,15 +143,15 @@ fn test_lock_inscription_fails_prior_lock_not_expired() {
#[test]
#[should_panic]
fn test_lock_inscription_fails_inscription_not_found() {
- let (orderbook_dispatcher, contract_address, token_dispatcher, _) = setup();
+ let (orderbook_dispatcher, token_dispatcher) = setup();
let test_taproot_address: ByteArray = "test";
let test_data: ByteArray = "data";
- token_dispatcher.approve(contract_address, 100);
+ token_dispatcher.approve(orderbook_dispatcher.contract_address, 100);
let _ = orderbook_dispatcher
- .request_inscription(test_data, test_taproot_address, 1, 'STRK'.into(), 10);
+ .request_inscription(test_data, test_taproot_address, 'STRK'.into(), 10);
orderbook_dispatcher.lock_inscription(42, "hash");
}
@@ -147,19 +162,19 @@ fn test_lock_inscription_fails_status_closed() { // TODO: when `submit_inscripti
#[test]
fn test_cancel_inscription_works() {
- let (orderbook_dispatcher, contract_address, token_dispatcher, _) = setup();
+ let (orderbook_dispatcher, token_dispatcher) = setup();
let test_taproot_address: ByteArray = "test";
let test_data: ByteArray = "data";
- token_dispatcher.approve(contract_address, 100);
+ token_dispatcher.approve(orderbook_dispatcher.contract_address, 100);
let id = orderbook_dispatcher
- .request_inscription(test_data, test_taproot_address, 1, 'STRK'.into(), 10);
+ .request_inscription(test_data, test_taproot_address, 'STRK'.into(), 10);
- start_cheat_caller_address_global(contract_address);
+ start_cheat_caller_address_global(orderbook_dispatcher.contract_address);
// TODO: is this the correct way to set permissions?
- token_dispatcher.approve(contract_address, 100);
+ token_dispatcher.approve(orderbook_dispatcher.contract_address, 100);
stop_cheat_caller_address_global();
orderbook_dispatcher.cancel_inscription(id, 'STRK'.into());
@@ -168,15 +183,15 @@ fn test_cancel_inscription_works() {
#[test]
#[should_panic]
fn test_cancel_inscription_fails_locked() {
- let (orderbook_dispatcher, contract_address, token_dispatcher, _) = setup();
+ let (orderbook_dispatcher, token_dispatcher) = setup();
let test_taproot_address: ByteArray = "test";
let test_data: ByteArray = "data";
- token_dispatcher.approve(contract_address, 100);
+ token_dispatcher.approve(orderbook_dispatcher.contract_address, 100);
let id = orderbook_dispatcher
- .request_inscription(test_data, test_taproot_address, 1, 'STRK'.into(), 10);
+ .request_inscription(test_data, test_taproot_address, 'STRK'.into(), 10);
orderbook_dispatcher.lock_inscription(id, "hash");
orderbook_dispatcher.cancel_inscription(id, 'STRK'.into())
@@ -189,18 +204,18 @@ fn test_cancel_inscription_fails_closed() { // TODO: when `submit_inscription` i
#[test]
#[should_panic]
fn test_cancel_inscription_fails_canceled() {
- let (orderbook_dispatcher, contract_address, token_dispatcher, _) = setup();
+ let (orderbook_dispatcher, token_dispatcher) = setup();
let test_taproot_address: ByteArray = "test";
let test_data: ByteArray = "data";
- token_dispatcher.approve(contract_address, 100);
+ token_dispatcher.approve(orderbook_dispatcher.contract_address, 100);
let id = orderbook_dispatcher
- .request_inscription(test_data, test_taproot_address, 1, 'STRK'.into(), 10);
+ .request_inscription(test_data, test_taproot_address, 'STRK'.into(), 10);
- start_cheat_caller_address_global(contract_address);
- token_dispatcher.approve(contract_address, 100);
+ start_cheat_caller_address_global(orderbook_dispatcher.contract_address);
+ token_dispatcher.approve(orderbook_dispatcher.contract_address, 100);
stop_cheat_caller_address_global();
orderbook_dispatcher.cancel_inscription(id, 'STRK'.into());
diff --git a/packages/onchain/src/relay.cairo b/packages/onchain/src/relay.cairo
new file mode 100644
index 0000000..a723721
--- /dev/null
+++ b/packages/onchain/src/relay.cairo
@@ -0,0 +1 @@
+mod relay;
diff --git a/packages/onchain/src/relay/relay.cairo b/packages/onchain/src/relay/relay.cairo
new file mode 100644
index 0000000..0f10308
--- /dev/null
+++ b/packages/onchain/src/relay/relay.cairo
@@ -0,0 +1,61 @@
+use utils::hash::Digest;
+use utu_relay::bitcoin::block::BlockHeader;
+
+#[starknet::interface]
+pub trait ITransactionInclusion {
+ fn prove_inclusion(
+ ref self: TContractState,
+ tx_id: Digest,
+ block_height: u64,
+ block_header: BlockHeader,
+ tx_inclusion: Array<(Digest, bool)>,
+ );
+}
+
+#[starknet::contract]
+mod TransactionInclusion {
+ use onchain::utils::utils::compute_merkle_root;
+ use utu_relay::bitcoin::block::BlockHashTrait;
+ use starknet::{ContractAddress, get_block_timestamp};
+ use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
+ use utils::{hash::Digest, numeric::u32_byte_reverse};
+ use utu_relay::{
+ interfaces::{IUtuRelayDispatcher, IUtuRelayDispatcherTrait}, bitcoin::block::BlockHeader,
+ };
+
+ #[storage]
+ struct Storage {
+ utu_address: ContractAddress,
+ }
+
+ #[constructor]
+ fn constructor(ref self: ContractState, utu_address: ContractAddress) {
+ self.utu_address.write(utu_address);
+ }
+
+ #[abi(embed_v0)]
+ impl TransactionInclusionImpl of super::ITransactionInclusion {
+ fn prove_inclusion(
+ ref self: ContractState,
+ tx_id: Digest,
+ block_height: u64,
+ block_header: BlockHeader,
+ tx_inclusion: Array<(Digest, bool)>,
+ ) {
+ // we verify this tx is included in the provided block
+ let merkle_root = compute_merkle_root(tx_id, tx_inclusion);
+ assert(
+ block_header.merkle_root_hash.value == merkle_root.value,
+ 'Invalid inclusion proof.',
+ );
+
+ // we verify this block is safe to use (part of the canonical chain & sufficient pow)
+ // sufficient pow for our usecase: 100 sextillion expected hashes
+ let utu = IUtuRelayDispatcher { contract_address: self.utu_address.read() };
+ utu.assert_safe(block_height, block_header.hash(), 100_000_000_000_000_000_000_000, 0);
+ // we ensure this block was not premined
+ let block_time = u32_byte_reverse(block_header.time).into();
+ assert(block_time <= get_block_timestamp(), 'Block comes from the future.');
+ }
+ }
+}
diff --git a/packages/onchain/src/utils/constants.cairo b/packages/onchain/src/utils/constants.cairo
index b586186..392a227 100644
--- a/packages/onchain/src/utils/constants.cairo
+++ b/packages/onchain/src/utils/constants.cairo
@@ -13,3 +13,7 @@ pub fn SYMBOL() -> ByteArray {
pub fn OWNER() -> ContractAddress {
contract_address_const::<'owner'>()
}
+
+pub fn UTU() -> ContractAddress {
+ contract_address_const::<'utu'>()
+}
diff --git a/packages/onchain/src/utils/utils.cairo b/packages/onchain/src/utils/utils.cairo
index 8b13789..b5de110 100644
--- a/packages/onchain/src/utils/utils.cairo
+++ b/packages/onchain/src/utils/utils.cairo
@@ -1 +1,37 @@
+use utils::hash::Digest;
+use utils::double_sha256::double_sha256_parent;
+/// Computes the Merkle root from a transaction hash and its siblings.
+///
+/// Arguments:
+/// - `tx_hash: Digest`: The transaction hash as a Digest
+/// - `siblings: Array<(Digest, bool)>`: An array of tuples (Digest, bool), where the bool indicates
+/// if the sibling is on the right
+///
+/// Returns:
+/// - `Digest`: The computed Merkle root as a Digest
+pub fn compute_merkle_root(tx_hash: Digest, siblings: Array<(Digest, bool)>) -> Digest {
+ let mut current_hash = tx_hash;
+
+ // Iterate through all siblings
+ let mut i = 0;
+ loop {
+ if i == siblings.len() {
+ break;
+ }
+
+ let (sibling, is_left) = *siblings.at(i);
+
+ // Concatenate current_hash and sibling based on the order
+ current_hash =
+ if is_left {
+ double_sha256_parent(@sibling, @current_hash)
+ } else {
+ double_sha256_parent(@current_hash, @sibling)
+ };
+
+ i += 1;
+ };
+
+ current_hash
+}