My First Ownable
This guide walks through creating your first custom Ownable package based on ownables/basic.
Prerequisitesâ
- The SDK is already set up in
ownables-sdk - Dependencies are installed with
yarn install - Rust toolchain setup is completed with
yarn rustup
1. Copy, Build, and Issue the packageâ
Copy the starter Ownable:
yarn ownables:copy basic my-first
Build the Ownable package:
yarn ownables:build my-first
Then open the Ownables SDK wallet and issue it:
- Click Issue an Ownable
- Click Upload package
- Select
ownables/my-first.zip - Select My first from the available packages
2. Customize the widgetâ
Edit ownables/my-first/Cargo.toml to set the description and bump the version:
description = "Happiness cannot be traveled to, owned, earned, worn or consumed. Happiness is the experience of living every minute with love, grace, and gratitude."
version = "0.1.1"
Then replace ownables/my-first/assets/index.html with:
<!--<!DOCTYPE html>-->
<html>
<head>
<style>
html, body { margin: 0; height: 100%; }
body { width: 100%; height: 100%; overflow: hidden; }
div { text-align: center; line-height: 100vh; font-size: 30vh; }
div:hover .off { display: none; }
div:not(:hover) .on { display: none; }
</style>
</head>
<body>
<div>
<span class="off">đ</span>
<span class="on">đ</span>
</div>
</body>
</html>
Rebuild after this change:
yarn ownables:build my-first
In the SDK wallet import my-first.zip again. After the import, the version number should be v0.1.1. Issue a new "My First" Ownable.
3. Interactive widgetâ
Edit ownables/my-first/Cargo.toml again and bump:
version = "0.1.2"
Then replace ownables/my-first/assets/index.html again with:
<html>
<head>
<style>
html, body { margin: 0; height: 100%; }
body { width: 100%; height: 100%; overflow: hidden; display: flex; align-items: center; justify-content: center; font-family: sans-serif; }
.container { text-align: center; }
.emoji { font-size: 30vh; line-height: 1; margin-bottom: 3vh; }
.controls { display: flex; justify-content: center; gap: 1rem; }
button { width: 100%; font-size: 2rem; padding: 0.4rem 0.8rem; border: 1px solid #d1d5db; border-radius: 0.6rem; background: #fff; cursor: pointer; }
button:active { transform: translateY(1px); }
</style>
</head>
<body>
<div class="container">
<div id="emoji" class="emoji"></div>
<div class="controls">
<button id="cloud" aria-label="Sadder">âī¸</button>
<button id="sun" aria-label="Happier">âī¸</button>
</div>
</div>
<script>
const moods = ["đ", "âšī¸", "đ", "đ", "đ"];
let moodIndex = 2;
const emojiEl = document.getElementById("emoji");
const cloudBtn = document.getElementById("cloud");
const sunBtn = document.getElementById("sun");
const renderMood = () => {
emojiEl.textContent = moods[moodIndex];
};
cloudBtn.addEventListener("click", () => {
moodIndex = Math.max(0, moodIndex - 1);
renderMood();
});
sunBtn.addEventListener("click", () => {
moodIndex = Math.min(moods.length - 1, moodIndex + 1);
renderMood();
});
renderMood();
</script>
</body>
</html>
Rebuild after this change:
yarn ownables:build my-first
In the SDK wallet import my-first.zip again. After the import, the version number should be v0.1.2. Issue a new "My First" Ownable.
4. Persist happiness with events and widget stateâ
Right now, happiness is only a JavaScript variable (moodIndex) in the widget. It is not stored on-chain in the Ownable state. That means after a refresh (or reopening the ownable), mood resets to the default.
To persist it:
- add execute events that mutate contract state
- store mood in
Config - return mood from
GetWidgetState - make widget buttons send execute messages instead of only changing local DOM state
4.1 Bump versionâ
Edit ownables/my-first/Cargo.toml:
version = "0.1.3"
4.2 Update widget to send execute eventsâ
In ownables/my-first/assets/index.html, stop mutating moodIndex directly in click handlers. Instead, send execute messages:
<script>
let ownable_id;
const moods = ["đ", "âšī¸", "đ", "đ", "đ"];
const emojiEl = document.getElementById("emoji");
const cloudBtn = document.getElementById("cloud");
const sunBtn = document.getElementById("sun");
const renderMood = (moodIndex) => {
emojiEl.textContent = moods[moodIndex] ?? '';
};
cloudBtn.addEventListener("click", () => {
window.parent.postMessage({type: "execute", ownable_id, msg: { "decrement": {} }}, "*");
});
sunBtn.addEventListener("click", () => {
window.parent.postMessage({type: "execute", ownable_id, msg: { "increment": {} }}, "*");
});
window.addEventListener("message", (event) => {
ownable_id = event.data.ownable_id;
const moodIndex = event.data?.state?.mood ?? 2;
renderMood(moodIndex);
});
</script>
4.3 Add mood to contract stateâ
Edit ownables/my-first/src/state.rs. We want to persist mood as number (0 to 4). We do that by adding it to the Config.
pub struct Config {
pub mood: i8,
}
4.4 Add execute messages for mood updatesâ
Edit ownables/my-first/src/msg.rs to add 2 execute messages.
pub enum ExecuteMsg {
Increment {},
Decrement {},
}
The increment and decrement messages don't have any properties.
4.5 Mutate and expose mood in the contractâ
Edit ownables/my-first/src/contract.rs:
- In
instantiate, initialize config with the middle mood.
CONFIG.save(deps.storage, &Config { mood: 2 })?;
- In
execute, handle new events.
match msg {
ExecuteMsg::Transfer { to } => try_transfer(info, deps, to),
ExecuteMsg::Increment {} => try_update_mood(info, deps, 1),
ExecuteMsg::Decrement {} => try_update_mood(info, deps, -1),
}
The execute function should have minimum logic and always call internal helper functions.
- Import helper functions.
On top of the file, import the function ensure_owner from the ownable_std.
use ownable_std::{package_title_from_name, ExternalEventMsg, InfoResponse, Metadata, OwnableInfo, ensure_owner};
With this function we ensure that the message is signed by the current owner.
- Implement execute method.
Calling ensure_owner is almost always be the first step of an execute method. Next we update the config with the new mood value (clamping to min/max).
fn try_update_mood(info: MessageInfo, deps: DepsMut, delta: i8) -> Result<Response, ContractError> {
let ownership = OWNABLE_INFO.load(deps.storage)?;
ensure_owner(&ownership, &info.sender, || ContractError::Unauthorized {
val: "Unauthorized mood update attempt".to_string(),
})?;
CONFIG.update(deps.storage, |mut config| -> Result<_, ContractError> {
let updated = config.mood + delta;
config.mood = updated.clamp(0, 4);
Ok(config)
})?;
Ok(Response::new())
}
4.6 Rebuild and re-importâ
yarn ownables:build my-first
Import ownables/my-first.zip again and issue a new "My First" Ownable. You should now see mood survive refreshes because state is event-driven and queried from the contract, not kept only in widget memory.