Kipt: easy contract management on Starknet
Kipt was develop to considerably ease the smart contracts management on Starknet.
"Ki" is from the Ki energy from a famous manga combined with the work "script", to illustrate the fact that Kipt's simplicity empowers the user to easily script his contracts declare, deploy and transact operations.
Leveraging the simplicity of Lua and the performance of Rust,
Kipt abstracts the whole complexity of interacting with the blockchain (with starknet-rs
).
Basically, Kipt is capable of executing Lua code, where special functions are made available
out of the box for the user like declare
, deploy
, invoke
and call
functions related
to Starknet.
Installation and run
The easiest way to install Kipt is to use kiptup
, a portable script that downloads prebuilt binaries and manages shell configuration for you. However, it might not be available depending on your device's platform and/or architecture.
Using kiptup
If you're on Linux/macOS/WSL, you can install kiptup
by running the following command:
curl https://raw.githubusercontent.com/glihm/kipt/main/kiptup/install | sh
You might need to restart your shell session for the kiptup
command to become available. Once it's available, run the kiptup
command:
kiptup
Running the commands installs kipt
for you, and upgrades it to the latest release if it's already installed.
ℹ️ Note
Over time,
kiptup
itself may change and require upgrading. To upgradekiptup
itself, run thecurl
command above again.
Prebuilt binaries
Prebuilt binaries are available with GitHub releases for certain platforms.
Prebuilt binaries are best managed with kiptup
. However, if you're on a platform where kiptup
isn't available (e.g. using kiptup
on Windows natively), you can manually download the prebuilt binaries and make them available from PATH
.
Run Kipt
For now Kipt as very few options, and you only have to provide the Lua script to execute:
kipt ./scripts/demo.lua
Basics
Some Lua and Kipt basics to get started with the scripting:
Lua basics
In Lua, only 8 types are used. In the context of Kipt, you only need 5 of them if you want to keep it simple:
nil
: nothing / no value.number
string
table
boolean
Except for table
, the other types are very common.
You can notice that there are no array
, but table
is flexible enough to be used as hashmap
and array
.
-- nil
local a = nil
-- Number
local b = 1234
-- String
local c = "kipt"
-- Boolean
local d = false
-- Table
local e = { a = 2, b = "abc" }
-- Table (array-like)
local f = { 1, 2, 3, 4 }
The use of local
is optional, but it's a good practice as any variable without local
is considered as global.
So if you use some functions for more advanced Lua programming, keep the use of local
.
In case of array, under the hood it's a table
, but index starting to 1
(nobody is perfect...).
To access member of an table
, you have two notations as showed below. If the key
does not exist,
it returns nil
.
local t = { a = 2, b = "abc" }
print(t.a)
print(t["b"])
if t.h then
-- h is not nil and has a value, do stuff with it.
end
To test nil
, the best practice is to compare with nil
, this is because nil
and false
are evaluated the same way:
local a = false
local b = nil
if not a and not b then
print("will be printed")
end
if a == nil and b == nil then
print("will not be printed")
end
This are all the basics you need to get started.
Global variables and setup
To interact with Rust, Kipt must retrieve some variables from Lua.
Any variable without the local
keyword are accessible from Rust.
For this purposes, some variables are automatically checked by Kipt to add some configuration:
-- No local keyword, they are global variables.
RPC = "GOERLI-1"
ACCOUNT_ADDRESS = "0x1234...."
ACCOUNT_PRIVKEY = "0x8888...."
-- If you're using a cairo 0 account, you will need legacy encoding.
-- ACCOUNT_IS_LEGACY = true
-- You can re-defined anywhere in the script a new value
RPC = "http://0.0.0.0:5050"
As shown, some global variables are expected by Kipt:
-
RPC
: can be any valid URL that starts withhttp
or some pre-defined networks. The supported networks are:MAINNET
: to use the gateway for the mainnet.GOERLI-1
: to use the gateway for goerli-1.GOERLI-2
: to use the gateway for goerli-2.KATANA
: to use the Katana in local on the default porthttp://0.0.0.0:5050
.
-
ACCOUNT_ADDRESS
: The address of the account to use to send transactions. -
ACCOUNT_PRIVKEY
: The private key of the account to use to send transactions. -
ACCOUNT_IS_LEGACY
: Specifies if the account is a cairo 0 account.
ℹ️ Note
Those variables are re-evaluated before each transaction. This means that you can change their value during the script without any problem to send some transactions with different accounts, to different URLs.
As Lua is a scripting language, it also has some capabilities. To not write your private key in plain text or pre-configured (CI for instance) the account and network with environment variables, you can do the following:
RPC = os.getenv("STARKNET_RPC")
ACCOUNT_PRIVKEY = os.getenv("STARKNET_KEY")
Logger
To ensure that all the transactions are recorded automatically, you have to initialize the logger and Kipt will generated a report for you.
-- Without arguments, this will output everything in the file `kipt.out`.
local logger = logger_init()
-- With the name you may prefer:
local logger = logger_init("my_output.txt")
⚠️ Warning
Kipt will overwrite the file if it already exist. Ensure to provide a different name to avoid data loss.
ℹ️ Note
Calling
logger_init()
multiple times with or without an argument will take effect only at the first call. All other calls are ignored.
By default, Kipt will write in the output file any transaction hash and output of the declare
, deploy
and invoke
functions.
If you want to output additional information, you can do the following:
logger:write("your content here")
Declare
Declare a class-hash
on-chain.
declare("contract_name", opts)
-- @param contract_name - The contract name (string).
string
-- @param opts - Options for the transaction (table).
{
-- The tx watch interval in milliseconds (or nil to not wait the tx receipt).
watch_interval = number,
-- The path to locate contract artifacts. For now, this path is relative
-- to where you execute `kipt`. Be aware of that.
artifacts_path = string,
-- If the class is already declared, no error is returned, the declaration
-- is skipped and the class hash is returned. The default value is false.
skip_if_declared = bool,
-- Any other keys in the table are ignored.
}
-- @return - A table on success, string error otherwise.
{
-- The transaction hash (only if `skip_if_declared` is false).
tx_hash = string,
-- The declared class hash (Sierra class hash).
class_hash = string,
}
There are two class-hash
on Starknet:
- Sierra
class-hash
, which is the hash of the Sierra representation of the program, including the ABI. - Casm
class-hash
, which is the compiled hash of the program.
By class-hash
we regularly speak about the Sierra one. But to declare a contract, we also need the Casm class-hash
in order to ensure that the compilation of the Sierra code on-chain is lowered to the expected Casm class-hash
.
To ease the process of declaring a new class, you must first use Scarb to compile your contract.
Please ensure you've sierra
and casm
options enabled in your Scarb.toml
file.
[[target.starknet-contract]]
# Enable Sierra codegen.
sierra = true
# Enable CASM codegen.
casm = true
This will generated two files (called contracts artifacts
):
mycontract.contract_class.json
(Sierra)mycontract.compiled_contract_class.json
(Casm)
The two files will be loaded by Kipt. For this, you only need to provide the contract's name and the path to find the artifacts
(usually generated in target/dev
directory of your scarb package).
Example
local opts = {
watch_interval = 300,
artifacts_path = "./target/dev",
}
local decl_res, _ = declare("mycontract", opts)
print("Declare transaction hash:" .. decl_res.tx_hash)
print("Declared class_hash: " .. decl_res.class_hash)
-- If you want to check the error and exit on error:
local decl_res, err = declare("mycontract", opts)
if err then
print(err)
-- Use a non-zero value to indicate an error.
os.exit(1)
end
ℹ️ Note
Providing the path for artifacts and the contract's name, Kipt will search for
<contract_name>_contract_class.json
or<contract_name>_sierra.json
for the Sierra file. And for the Casm file, Kipt will search for<contract_name>_compiled_contract_class.json
or<contract_name>_casm.json
.
As you can see, in few lines of code you can control the transactions sent on Starknet.
And having the class-hash
from the declare, you can now easily deploy an instance of the contract.
Deploy
Deploy a contract instance for the given class-hash
.
deploy("class_hash", args, opts)
-- @param class_hash - The sierra class hash to deploy (string).
string
-- @param args - Arguments passed to the constructor during deployment (table array-like of strings).
{ string, string, ... }
-- @param opts - Options for the transaction (table).
{
-- The tx watch interval in milliseconds (or nil to not wait the tx receipt).
watch_interval = number,
-- The salt use to compute the contract address (or nil to use a random salt).
salt = string,
-- Any other keys in the table are ignored.
}
-- @return - A table on success, string error otherwise.
{
-- The transaction hash.
tx_hash = string,
-- The address of the deployed contract.
deployed_address = string,
}
For now, the felt252
type is represented as a string in Lua. So you have to pass the arguments
as serialized felts.
Work in progress: in the future, Kipt will also provider some basic scheme as starkli does.
Example
local opts = {
watch_interval = 300,
}
local decl_res, _ = declare("mycontract", opts)
local class_hash = decl_res.class_hash
-- If the constructor has no arguments, just use an emtpy table.
local args = {}
local depl_res, _ = deploy(class_hash, args, opts)
print("Deploy transaction hash:" .. depl_res.tx_hash)
print("Deployed address: " .. depl_res.deployed_address)
-- Add some arguments in array-like fashion:
local args = { "0x1234", "0x8822" }
local depl_res, _ = deploy(class_hash, args, opts)
Invoke
Sends an invoke transaction to Starknet.
invoke(calls, opts)
-- @param calls - A list of calls to be executed (table).
{
{
-- Contract to target.
to = string,
-- The function name to invoke.
func = string,
-- Arguments for the function (table array-like of strings)
calldata = { string, string ... },
},
...
}
-- @param opts - Options for the transaction (table).
{
-- The tx watch interval in milliseconds (or nil to not wait the tx receipt).
watch_interval = number,
-- Any other keys in the table are ignored.
}
-- @return - A table on success, string error otherwise.
{
-- The transaction hash.
tx_hash = string,
}
For now, the felt252
type is represented as a string in Lua. So you have to pass the arguments
as serialized felts.
Work in progress: in the future, Kipt will also provider some basic scheme as starkli does.
Example
local opts = {
watch_interval = 300,
}
local invk_res, _ = invoke(
{
{
to = "0x1111",
func = "set_a",
calldata = { "0x1234" },
},
},
opts
)
print("Invoke TX hash: " .. invk_res.tx_hash)
Call
Realizes a function call on Starknet.
call("contract_address", "function_name", args, opts)
-- @param contract_address - The contract to target (string).
string
-- @param function_name - The name of the function to call (string).
string
-- @param args - Arguments passed to function (table array-like of strings).
{ string, string, ... }
-- @param opts - Options for the transaction (table).
{
-- The block id against which the function call is done. Can be "pending", "latest" or any number in decimal.
-- Default = "pending".
block_id = string,
-- Any other keys in the table are ignored.
}
-- @return - A table array-like of strings on success, string error otherwise.
{ string, string, ... }
For now, the felt252
type is represented as a string in Lua. So you have to pass the arguments
as serialized felts.
Work in progress: in the future, Kipt will also provider some basic scheme as starkli does.
The output of the call is also the serialized list of felts as string
.
Example
local call_res, _ = call(contract_address, "get_a", {}, { block_id = "latest" })
print_str_array(call_res)
ℹ️ Note
Note the usage of
print_str_array
, this is a function provided by Kipt to easily print a formatted array of felts returns by a call.
Watch Transaction
Polls the receipt of the given transaction hash.
watch_tx("tx_hash", interval_ms)
-- @param tx_hash - The transaction hash (string).
string
-- @param interval_ms - The interval in milliseconds to poll the receipt (number).
number
As you've seen for transaction based functions (like declare, deploy and invoke) you already have an option you can pass to wait for the transaction receipt before continuing.
However, in some cases, you may want to send several transactions, but you don't want declare, deploy and invoke to block until the receipt is available.
For this, you can set the watch_interval
of such functions to nil
(or simply remove this key from the options), and then you can manually watch any transaction you would like.
Example
-- Note here the absence of `watch_interval` key as options is empty.
local set_a_res, _ = invoke(
{
{
to = "0x1111",
func = "set_a",
calldata = { "0x1234" },
},
},
{}
)
-- the first invoke will not block, and will return
-- once the transaction hash is obtained from the RPC.
-- But the transaction may not be executed at the moment
-- we call this one.
local set_b_res, _ = invoke(
{
{
to = "0x1111",
func = "set_b",
calldata = { "0xff" },
},
},
{}
)
watch_tx(set_b_res.tx_hash, 200);
-- once here, we're sure that the second invoke transaction
-- was processed by the sequencer.
All functions
An example using all functions:
RPC = "KATANA"
ACCOUNT_ADDRESS = "0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973"
ACCOUNT_PRIVKEY = "0x1800000000300000180000000000030000000000003006001800006600"
-- No args -> kipt.out
local logger = logger_init()
-- Reuse this opts in each one as each function
-- only looks for it's configuration options. The other
-- are ignored.
local decl_res, err = declare(
"mycontract",
{ watch_interval = 300, artifacts_path = "./target/dev" }
)
if err then
print(err)
os.exit(1)
end
print("Declared class_hash: " .. decl_res.class_hash)
-- Deploy with no constructor args.
local depl_res, err = deploy(decl_res.class_hash, {}, { watch_interval = 300, salt = "0x1234" })
if err then
print(err)
os.exit(2)
end
local contract_address = depl_res.deployed_address
print("Contract deployed at: " .. contract_address)
-- Invoke to set a value.
local invk_res, err = invoke(
{
{
to = contract_address,
func = "set_a",
calldata = { "0x1234" },
},
},
{ watch_interval = 300 }
)
if err then
print(err)
os.exit(3)
end
print("Invoke TX hash: " .. invk_res.tx_hash)
local call_res, err = call(contract_address, "get_a", {}, {})
print_str_array(call_res)
The output auto-generated by Kipt due to the use of logger_init()
is the following:
-- Friday, November 3, 2023 02:24:58 --
> declare: mycontract
| tx_hash | 0x003fc5e719e67eedcefd3f1ac9902f5d09a1e1839d99441b8d52e4564879255f |
| class_hash | 0x06013bb81a928bd226416a34bdfabd7a69b43bab2911a8ae3647a4ba9706bfc0 |
> deploy: 0x06013bb81a928bd226416a34bdfabd7a69b43bab2911a8ae3647a4ba9706bfc0
| tx_hash | 0x008fdb1a82b1f5b76597a45919def8d592c6a4de99378897090031d4c63ab712 |
| deployed address | 0x0128c14a284dac9224afff4ad9c8a071e561932166c2ced2544ed0c67b4cecce |
> invoke: 1
call #0 -> 0x0128c14a284dac9224afff4ad9c8a071e561932166c2ced2544ed0c67b4cecce set_a
| tx_hash | 0x00420beed7f7395f29ce81595ffc35cb97732a6d83b298208cf8f3df621e8f95 |