In the previous article, we made a cross-chain transaction, and now we have some KDA on two chains. So, let’s deploy our first contract to create our token on Kadena Testnet! It will be a fun journey.
Step 1: Select “Contracts” from the left menu in Chainweaver.
Step 2: Go to Appendix 1 at the end or Thanos420NoScope GitHub’s Anedak folder under Contracts/anedak.pact, copy its code and paste it under the contract editor on Chainweaver. Since the developers have updated the pact language, the anedak.pact from Thanos420NoScope seems dated. The (namespace “free”) should be at the first line. You may compare my updated code in Appendix 1 (at the end of this article) with Thanos420NoScope’s code.
Step 3: Every module should have a unique name. Therefore you should change your module to another name instead of eth in step 2 LINE 5. You may name it whatever you, like such as whatsoeveryyyy or testingyyyymmdd. Also, the “admin-eth” should be also changed, for example, you may change LINE 3 to (define-keyset ‘admin-whatsoeveryyyy (read-keyset “admin-whatsoeveryyyy”)) or (define-keyset ‘admin-testingyyyymmdd (read-keyset “admin-testingyyyymmdd”)). Just follow your naming pattern.
Step 4: Change the admin-eth to admin-testingyyyymmdd, admin-whatsoeveryyyy, or your naming pattern on LINE 42.
Step 5: LINE 103 shows which account will receive all the tokens before initialisation. In this case, the ROOT account will obtain all the tokens before initialization; leave it alone. Finally, LINE 106 shows the initial supply of the token in 1 chain. To save some time, we will only deploy this contract in chain 0. You should divide the total supply by 20 as you have to deploy it in 20 chains in real-life applications. Otherwise, other people may deploy a contract with the same name but using their address, pretending it is your token. Feel free to correct me if I have misunderstood something.
Step 6: We will find an error under the ENV page. Press the Fix button.
Step 7: Tick your public key under the admin-xxx field.
Step 8: Click the “Deploy” button on the top-right. You may ignore the “Cannot resolve fungible-v2.account-details” error.
Step 9: Select Chain 0 (or any chain you like, but remember to have some KDA on that chain for paying the gas fee. Check out the cross-chain transaction on the Kadena article in case you forget it). Enter the account name you created on the first article. In my case, it’s silicon.blog. Click the “Next” button.
Step 10: Select your public key under the Signing key section. After that, click the “Next” button
Step 11: You may find an error under the raw response field. The gas fee you enter on the configuration page is not enough. Go back to the configuration page and edit the gas limit so that it is larger than the required amount (In my case, it should be more significant than 62049, setting 70000 as the gas limit is safe for me). Go back to the preview page after modifying the gas limit.
Step 12: This time, the message under raw response is “TableCreated“. We are ready to go. Click the “Submit” button. If you receive Error from (devnet.kaddex.com): : Failure: Tx Failed: Keyset failure (keys-all) error, you should go back to step 3 and give your module a new name; someone created its name before.
Step 13: After a few minutes, you will receive the “TableCreated” under Transaction Result.
Step 14: On the right side of the contract page, you will find a tab called “MODULE EXPLORER“. Click on it. Enter the text after the define-keyset you set in your contract (LINE 5). In my case, it is “free.eth” on chain “0”. You may need to wait a few minutes before it appears on the module explorer. Click the refresh button if you cannot find your contract. Click the “View” button after your contract appears on the search list.
Step 15: Under the Functions section, there is a function called “initialize”, click the “call” button.
Step 16: Enter your account name, and click the “Next” button.
Step 17: On the sign page, you should click the “+” button and add (free.<your token name>.GOVERNANCE) under the Capability field. In my case, it is free.eth.GOVERNANCE. Select your public key under the signing key field. Click the “Next Button“
Step 18: You should receive a “Write Succeeded” message under Raw Response. Check your spelling/token name if you receive an error message. Click the “Submit” button if there is no error.
Step 19: After a few minutes, you will receive a “Write succeeded” under Transaction Result. Click the “Done” button.
Step 20: Click the move-premine function’s “Call” button.
Step 21: Enter your account name as the receiver. Input (read-keyset “admin-<your token>”) as a guard. You can find it on LINE 3 of your contract. In my case, it is read-keyset “admin-eth”. Enter the total amount you set in LINE 106 on your contract as the amount. Click the “Next” button after finish. REMEMBER YOU CAN ONLY TRANSFER THE TOKEN FROM ROOT TO YOUR ACCOUNT ONCE, MAKE SURE YOU DON”T ENTER THE WRONG ACCOUNT NAME & TOTAL AMOUNT.
Step 22: Enter your account name as the Transaction sender. Click the “Next” button.
Step 23: Same as step 17, enter the required information and click the “Next” button.
Step 24: You should receive “Write succeeded” under Raw Response. Click the “Submit” button.
Step 25: If you receive “Write Succeeded” under the transaction result. Congratulations! You have created your token successfully in Kadena!
We have created our token! In the following article, we will learn how to check the account balance of the new token in our account and transfer it to other people on Chainweaver.
If you think this article is helpful, leave a comment below. 🙂
You may also sponsor this website by sending some KDA to this address: k:e95a044488f4b6ced42ab9be0268ffe530e47401b24b8c29bfeba5942e5da275.
Appendix 1
(namespace "free")
(define-keyset "free.admin-eth" (read-keyset "admin-eth"))
(module eth GOVERNANCE
@doc " 'Anedak' is the first token implemented on the Kadena mainnet \
\ Adapted from Kadena's coin.pact contract and finprint. "
@model
[ (defproperty conserves-mass (amount:decimal)
(= (column-delta token-table 'balance) 0.0))
(defproperty valid-account-id (accountId:string)
(and
(>= (length accountId) 3)
(<= (length accountId) 256)))
]
(implements fungible-v2)
; --------------------------------------------------------------------------
; Schemas and Tables
(defschema token-schema
@doc " An account, holding a token balance. \
\ \
\ ROW KEY: accountId. "
balance:decimal
guard:guard
)
(deftable token-table:{token-schema})
; --------------------------------------------------------------------------
; Capatilibites
(defcap GOVERNANCE
()
@doc " Give the admin full access to call and upgrade the module. "
(enforce-keyset "free.admin-eth")
)
(defcap INTERNAL ()
@doc "only for internal use"
true
)
(defcap ACCOUNT_GUARD
( accountId:string )
@doc " Look up the guard for an account, required to debit from that account. "
(enforce-guard (at 'guard (read token-table accountId ['guard])))
)
(defcap DEBIT
( sender:string )
@doc " Capability to perform debiting operations. "
(enforce-guard (at 'guard (read token-table sender ['guard])))
(enforce (!= sender "") "Invalid sender.")
)
(defcap CREDIT
( receiver:string )
@doc " Capability to perform crediting operations. "
(enforce (!= receiver "") "Invalid receiver.")
)
(defcap TRANSFER:bool
( sender:string
receiver:string
amount:decimal )
@doc " Capability to perform transfer between two accounts. "
@managed amount TRANSFER-mgr
(enforce (!= sender receiver) "Sender cannot be the receiver.")
(enforce-unit amount)
(enforce (> amount 0.0) "Transfer amount must be positive.")
(compose-capability (DEBIT sender))
(compose-capability (CREDIT receiver))
)
(defun TRANSFER-mgr:decimal
( managed:decimal
requested:decimal )
(let ((newbal (- managed requested)))
(enforce (>= newbal 0.0)
(format "TRANSFER exceeded for balance {}" [managed]))
newbal
)
)
; --------------------------------------------------------------------------
; Constants
(defconst ROOT_ACCOUNT_ID:string 'ROOT
" ID for the account which initially owns all the tokens. ")
(defconst INITIAL_SUPPLY:decimal 122771325.0
" Initial supply of 21 million tokens. (122771325 x1 chains)")
(defconst DECIMALS 12
" Specifies the minimum denomination for token transactions. ")
(defconst ACCOUNT_ID_CHARSET CHARSET_LATIN1
" Allowed character set for account IDs. ")
(defconst ACCOUNT_ID_PROHIBITED_CHARACTER "$")
(defconst ACCOUNT_ID_MIN_LENGTH 3
" Minimum character length for account IDs. ")
(defconst ACCOUNT_ID_MAX_LENGTH 256
" Maximum character length for account IDs. ")
; --------------------------------------------------------------------------
; Utilities
(defun validate-account-id
( accountId:string )
@doc " Enforce that an account ID meets charset and length requirements. "
(enforce
(is-charset ACCOUNT_ID_CHARSET accountId)
(format
"Account ID does not conform to the required charset: {}"
[accountId]))
(enforce
(not (contains accountId ACCOUNT_ID_PROHIBITED_CHARACTER))
(format "Account ID contained a prohibited character: {}" [accountId]))
(let ((accountLength (length accountId)))
(enforce
(>= accountLength ACCOUNT_ID_MIN_LENGTH)
(format
"Account ID does not conform to the min length requirement: {}"
[accountId]))
(enforce
(<= accountLength ACCOUNT_ID_MAX_LENGTH)
(format
"Account ID does not conform to the max length requirement: {}"
[accountId]))
)
)
;; ; --------------------------------------------------------------------------
;; ; Fungible-v2 Implementation
(defun transfer-create:string
( sender:string
receiver:string
receiver-guard:guard
amount:decimal )
@doc " Transfer to an account, creating it if it does not exist. "
@model [ (property (conserves-mass amount))
(property (> amount 0.0))
(property (valid-account-id sender))
(property (valid-account-id receiver))
(property (!= sender receiver)) ]
(with-capability (TRANSFER sender receiver amount)
(debit sender amount)
(credit receiver receiver-guard amount)
)
)
(defun transfer:string
( sender:string
receiver:string
amount:decimal )
@doc " Transfer to an account, failing if the account does not exist. "
@model [ (property (conserves-mass amount))
(property (> amount 0.0))
(property (valid-account-id sender))
(property (valid-account-id receiver))
(property (!= sender receiver)) ]
(with-read token-table receiver
{ "guard" := guard }
(transfer-create sender receiver guard amount)
)
)
(defun debit
( accountId:string
amount:decimal )
@doc " Decrease an account balance. Internal use only. "
@model [ (property (> amount 0.0))
(property (valid-account-id accountId))
]
(validate-account-id accountId)
(if (= accountId ROOT_ACCOUNT_ID) (require-capability (INTERNAL)) true)
(enforce (> amount 0.0) "Debit amount must be positive.")
(enforce-unit amount)
(require-capability (DEBIT accountId))
(with-read token-table accountId
{ "balance" := balance }
(enforce (<= amount balance) "Insufficient funds.")
(update token-table accountId
{ "balance" : (- balance amount) }
)
)
)
(defun credit
( accountId:string
guard:guard
amount:decimal )
@doc " Increase an account balance. Internal use only. "
@model [ (property (> amount 0.0))
(property (valid-account-id accountId))
]
(validate-account-id accountId)
(enforce (> amount 0.0) "Credit amount must be positive.")
(enforce-unit amount)
(require-capability (CREDIT accountId))
(with-default-read token-table accountId
{ "balance" : -1.0, "guard" : guard }
{ "balance" := balance, "guard" := retg }
; we don't want to overwrite an existing guard with the user-supplied one
(enforce (= retg guard)
"account guards do not match")
(let ((is-new
(if (= balance -1.0)
(enforce-reserved accountId guard)
false)))
(write token-table accountId
{ "balance" : (if is-new amount (+ balance amount))
, "guard" : retg
}))
))
(defun check-reserved:string (accountId:string)
" Checks ACCOUNT for reserved name and returns type if \
\ found or empty string. Reserved names start with a \
\ single char and colon, e.g. 'c:foo', which would return 'c' as type."
(let ((pfx (take 2 accountId)))
(if (= ":" (take -1 pfx)) (take 1 pfx) "")))
(defun enforce-reserved:bool (accountId:string guard:guard)
@doc "Enforce reserved account name protocols."
(let ((r (check-reserved accountId)))
(if (= "" r) true
(if (= "k" r)
(enforce
(= (format "{}" [guard])
(format "KeySet {keys: [{}],pred: keys-all}"
[(drop 2 accountId)]))
"Single-key account protocol violation")
(enforce false
(format "Unrecognized reserved protocol: {}" [r]))))))
(defschema crosschain-schema
@doc " Schema for yielded value in cross-chain transfers "
receiver:string
receiver-guard:guard
amount:decimal
)
(defpact transfer-crosschain:string
( sender:string
receiver:string
receiver-guard:guard
target-chain:string
amount:decimal )
@model [ (property (> amount 0.0))
(property (!= receiver ""))
(property (valid-account-id sender))
(property (valid-account-id receiver))
]
(step
(with-capability (DEBIT sender)
(validate-account-id sender)
(validate-account-id receiver)
(enforce (!= "" target-chain) "empty target-chain")
(enforce (!= (at 'chain-id (chain-data)) target-chain)
"cannot run cross-chain transfers to the same chain")
(enforce (> amount 0.0)
"transfer quantity must be positive")
(enforce-unit amount)
;; Step 1 - debit sender account on current chain
(debit sender amount)
(let
((
crosschain-details:object{crosschain-schema}
{ "receiver" : receiver
, "receiver-guard" : receiver-guard
, "amount" : amount
}
))
(yield crosschain-details target-chain)
)
)
)
(step
(resume
{ "receiver" := receiver
, "receiver-guard" := receiver-guard
, "amount" := amount
}
;; Step 2 - credit receiver account on target chain
(with-capability (CREDIT receiver)
(credit receiver receiver-guard amount)
)
)
)
)
(defun get-balance:decimal
( account:string )
(at 'balance (read token-table account ['balance]))
)
(defun details:object{fungible-v2.account-details}
( account:string )
(with-read token-table account
{ "balance" := balance
, "guard" := guard
}
{ "account" : account
, "balance" : balance
, "guard" : guard
}
)
)
(defun precision:integer
()
DECIMALS
)
(defun enforce-unit:bool
( amount:decimal )
@doc " Enforce the minimum denomination for token transactions. "
(enforce
(= (floor amount DECIMALS) amount)
(format "Amount violates minimum denomination: {}" [amount])
)
)
(defun create-account:string
( account:string
guard:guard )
@doc " Create a new account. "
@model [ (property (valid-account-id account)) ]
(validate-account-id account)
(enforce-reserved account guard)
(insert token-table account
{ "balance" : 0.0
, "guard" : guard
}
)
)
(defun rotate:string
( account:string
new-guard:guard )
(with-read token-table account
{ "guard" := oldGuard }
(if (= account ROOT_ACCOUNT_ID) (require-capability (INTERNAL)) true)
(enforce-guard oldGuard)
(enforce-guard new-guard)
(update token-table account
{ "guard" : new-guard }
)
)
)
;; ; --------------------------------------------------------------------------
;; ; Custom Functions
(defun initialize:string
()
@doc " Initialize the contract. \
\ Admin-only. Should fail if it has been called before. "
(with-capability (GOVERNANCE)
(create-account ROOT_ACCOUNT_ID (create-module-guard "root-account"))
(update token-table ROOT_ACCOUNT_ID { "balance" : INITIAL_SUPPLY })
)
)
(defun move-premine:string
( receiver:string
guard:guard
amount:decimal )
@doc " Admin-only. Move the premine. "
(with-capability (GOVERNANCE)
(with-capability (INTERNAL)
(install-capability (TRANSFER ROOT_ACCOUNT_ID receiver amount))
(transfer-create ROOT_ACCOUNT_ID receiver guard amount)
)
)
)
)
(create-table token-table)
Thanks for the tutorial. It has been a great help for the moment. Is there any way to change the token symbol? In the example it is free.eth, but if one wanted it to be just eth, what should be done?
Dear Diego,
Sorry for the late reply, I was on a vacation for a whole month.
You may check out the part 8 of my article here:
Deploy your own decentralized exchange on Kadena [8: create our own decentralized exchange based on the cloned Kaddex swap project]
You just need to change the icon on the tokens.json file.