Skip to main content

Overview

In this section, we walk you through how to implement the Chainrails Intent Broadcaster in your Cairo contracts on Starknet.

Step 1 — Import required interfaces and types

Import the broadcaster dispatcher, ERC20 dispatcher, and shared types.
Imports
use starknet::ContractAddress;
use starknet::{get_caller_address, get_contract_address};
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
use core::num::traits::Zero;
use crate::base::types::types::{BroadcastedIntent, Chain, TokenAmount};
use crate::interfaces::IERC20::{IERC20Dispatcher, IERC20DispatcherTrait};
use crate::mocks::interfaces::IMockIntentBroadCaster::{
	IIntentBroadCasterDispatcher, IIntentBroadCasterDispatcherTrait,
};
use crate::utils::address_utils::contract_address_to_u256;

Step 2 — Define your contract interface

Expose functions for broadcasting, canceling, and querying execution/escrow status.
Contract Interface
#[starknet::interface]
pub trait ISimpleIntentBroadcaster<TContractState> {
	fn broadcast_simple_intent(
		ref self: TContractState,
		source_chain: Chain,
		source_token: ContractAddress,
		amount: u256,
		destination_chain: Chain,
		destination_token: ContractAddress,
		recipient: u256,
		refund_address: ContractAddress,
		max_fee_budget: u256,
		is_live: bool,
	) -> felt252;

	fn cancel_intent(ref self: TContractState, broadcast_id: felt252);

	fn is_executed(self: @TContractState, broadcast_id: felt252) -> bool;

	fn get_escrowed_amount(self: @TContractState, broadcast_id: felt252) -> u256;
}

Step 3 — Add utility for token address encoding

Chainrails token representation uses u256, so convert Starknet ContractAddress to u256.
Address Utils
use core::felt252;
use core::integer::u256;
use starknet::ContractAddress;

pub fn contract_address_to_u256(address: ContractAddress) -> u256 {
	let felt_value: felt252 = address.try_into().unwrap();
	felt_value.try_into().unwrap()
}

Step 4 — Store broadcaster address and initialize it

Persist the broadcaster address in storage and validate it in constructor.
Storage and Constructor
#[storage]
struct Storage {
	broadcaster: ContractAddress,
}

#[constructor]
fn constructor(ref self: ContractState, broadcaster_address: ContractAddress) {
	assert(!broadcaster_address.is_zero(), 'Invalid broadcaster address');
	self.broadcaster.write(broadcaster_address);
}

Step 5 — Broadcast an intent

Create a function that escrows funds, approves broadcaster spend, builds BroadcastedIntent + deposits, and calls broadcast_intent.
Broadcast Intent
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
	IntentSent: IntentSent,
}

#[derive(Drop, starknet::Event)]
pub struct IntentSent {
	#[key]
	pub broadcast_id: felt252,
	#[key]
	pub sender: ContractAddress,
	pub amount: u256,
}

fn broadcast_simple_intent(
	ref self: ContractState,
	source_chain: Chain,
	source_token: ContractAddress,
	amount: u256,
	destination_chain: Chain,
	destination_token: ContractAddress,
	recipient: u256,
	refund_address: ContractAddress,
	max_fee_budget: u256,
	is_live: bool,
) -> felt252 {
	let total_deposit = amount + max_fee_budget;
	let caller = get_caller_address();
	let this_contract = get_contract_address();
	let broadcaster_address = self.broadcaster.read();

	// Pull source tokens from user into this contract.
	let token_dispatcher = IERC20Dispatcher { contract_address: source_token };
	token_dispatcher.transfer_from(caller, this_contract, total_deposit);

	// Allow broadcaster to escrow the tokens.
	token_dispatcher.approve(broadcaster_address, total_deposit);

	let bridge_token_out_options = array![
		TokenAmount { token: contract_address_to_u256(destination_token), amount },
	];

	let intent = BroadcastedIntent {
		source_chain,
		destination_chain,
		bridge_token_out_options,
		sender: caller,
		destination_recipient: recipient,
		refund_address,
	};

	let deposits = array![
		TokenAmount {
			token: contract_address_to_u256(source_token), amount: total_deposit,
		},
	];

	let broadcast_dispatcher = IIntentBroadCasterDispatcher {
		contract_address: broadcaster_address,
	};

	let broadcast_id =
		broadcast_dispatcher.broadcast_intent(intent, deposits, max_fee_budget, is_live);

	self.emit(IntentSent { broadcast_id, sender: caller, amount });

	broadcast_id
}

Cancellation and Refunds

Cancellation and refunds of broadcasted intents are handled automatically after intent expiration (typically around 1 hour after broadcasting). You can also support manual cancel from your broadcasting contract:
Cancel Broadcast
fn cancel_intent(ref self: ContractState, broadcast_id: felt252) {
	let broadcaster_address = self.broadcaster.read();
	let broadcast_dispatcher = IIntentBroadCasterDispatcher {
		contract_address: broadcaster_address,
	};
	broadcast_dispatcher.cancel_broadcast(broadcast_id);
}
NB: Only the broadcasting contract can manually cancel a broadcast before expiration, if it has not been executed. Once cancelled, escrowed funds for that broadcast are refunded to the configured refund_address.

Github Example

Find the full implementation of the above example in the GitHub repository.