Welcome to our guide on launching a token using the Koinos Contract Standard (KCS-4) for tokens and the AssemblyScript SDK for Koinos. In this tutorial, we'll walk you through the process of creating and deploying your own token on the Koinos blockchain. Whether you're a seasoned developer or new to blockchain development, this step-by-step guide will provide you with the knowledge and tools necessary to bring your token project to life. Let's dive in and explore the exciting world of token creation on Koinos!
Before starting, ensure that you have already set up your Koinos AssemblyScript SDK environment by following this guide.
Note
You should understand the requirements of your token before adhering to any particular standard. Visit the Koinos Contract Standards repository for more information.
Let's begin by creating a boilerplate smart contract project using the Koinos AssemblyScript SDK.
koinos-sdk-as-clicreatetoken
Generating contract at "/home/$USER/Workspace/token" ...
Contract successfully generated!
You're all set! Run the following set of commands to verify that the generated contract is correctly setup:
cd /home/$USER/Workspace/token && yarn install && yarn build:debug && yarn test
Change your directory to the project directory and install dependencies.
cdtoken
yarninstall
yarn install v1.22.19
warning ../../../../../package.json: No license field
info No lockfile found.
[1/4] 🔍 Resolving packages...
warning @koinos/sdk-as > @koinos/mock-vm > multibase@4.0.6: This module has been superseded by the multiformats module
warning @koinos/sdk-as > @koinos/mock-vm > somap > npm > @npmcli/ci-detect@2.0.0: this package has been deprecated, use `ci-info` instead
warning @koinos/sdk-as > @koinos/mock-vm > somap > npm > libnpmexec > @npmcli/ci-detect@2.0.0: this package has been deprecated, use `ci-info` instead
warning @koinos/sdk-as > @koinos/mock-vm > somap > npm > readdir-scoped-modules@1.1.0: This functionality has been moved to @npmcli/fs
warning @koinos/sdk-as > @koinos/mock-vm > somap > npm > @npmcli/arborist > readdir-scoped-modules@1.1.0: This functionality has been moved to @npmcli/fs
warning @koinos/sdk-as > @koinos/mock-vm > somap > npm > @npmcli/arborist > @npmcli/move-file@2.0.1: This functionality has been moved to @npmcli/fs
warning @koinos/sdk-as > @koinos/mock-vm > somap > npm > cacache > @npmcli/move-file@2.0.1: This functionality has been moved to @npmcli/fs
warning @koinos/sdk-as > @koinos/mock-vm > somap > npm > readdir-scoped-modules > debuglog@1.0.1: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
warning local-koinos > koilib > multibase@4.0.6: This module has been superseded by the multiformats module
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
warning "@koinos/sdk-as > @as-covers/core > @as-covers/transform > visitor-as@0.6.0" has incorrect peer dependency "assemblyscript@^0.18.31".
warning " > ts-node@10.9.2" has unmet peer dependency "@types/node@*".
warning Workspaces can only be enabled in private projects.
[4/4] 🔨 Building fresh packages...
success Saved lockfile.
✨ Done in 12.96s.
Building a contract will usually consist of behaviors and data. The behavior is defined by the smart contract. But the data is defined by Protobuf. We generate data structures with Protobuf so that the smart contract can easily integrate with other Koinos tools. We have defined the arguments and results we will use in our calculator. We use the *_arguments convention for contract function arguments and *_result for contract function results.
Let's begin by defining our entry point arguments and results.
viassembly/proto/token.proto
Currently, the token.proto file contains the following boilerplate code:
syntax="proto3";packagetoken;import"koinos/options.proto";messagebalance_object{uint64value=1;}// @description Returns the token's name// @read-only truemessagename_arguments{}messagename_result{stringvalue=1;}// @description Returns the token's symbol// @read-only truemessagesymbol_arguments{}messagesymbol_result{stringvalue=1;}// @description Returns the token's decimals precision// @read-only truemessagedecimals_arguments{}messagedecimals_result{uint32value=1;}// @description Returns the token's total supply// @read-only truemessagetotal_supply_arguments{}messagetotal_supply_result{uint64value=1[jstype=JS_STRING];}// @description Checks the balance at an address// @read-only truemessagebalance_of_arguments{bytesowner=1[(koinos.btype)=ADDRESS];}messagebalance_of_result{uint64value=1[jstype=JS_STRING];}// @description Returns the name, symbol, and decimals of the token// @read-only truemessageget_info_arguments{}messageget_info_result{stringname=1;stringsymbol=2;uint32decimals=3;}// @description Returns the allowance defined for a specific owner and account// @read-only truemessageallowance_arguments{bytesowner=1[(koinos.btype)=ADDRESS];bytesspender=2[(koinos.btype)=ADDRESS];}messageallowance_result{uint64value=1[jstype=JS_STRING];}messagespender_value{bytesspender=1[(koinos.btype)=ADDRESS];uint64value=2[jstype=JS_STRING];}// @description Returns a list of allowances for a given owner// @read-only truemessageget_allowances_arguments{bytesowner=1[(koinos.btype)=ADDRESS];bytesstart=2[(koinos.btype)=ADDRESS];int32limit=3;booldescending=4;}messageget_allowances_result{bytesowner=1[(koinos.btype)=ADDRESS];repeatedspender_valueallowances=2;}// @description Transfers the token// @read-only false// @result transfer_resultmessagetransfer_arguments{bytesfrom=1[(koinos.btype)=ADDRESS];bytesto=2[(koinos.btype)=ADDRESS];uint64value=3[jstype=JS_STRING];stringmemo=4;}messagetransfer_result{}// @description Mints the token// @read-only false// @result mint_resultmessagemint_arguments{bytesto=1[(koinos.btype)=ADDRESS];uint64value=2[jstype=JS_STRING];}messagemint_result{}// @description Burns the token// @read-only false// @result burn_resultmessageburn_arguments{bytesfrom=1[(koinos.btype)=ADDRESS];uint64value=2[jstype=JS_STRING];}messageburn_result{}// @description Adds an allownance for a given owner and account pairing// @read-only false// @result approve_resultmessageapprove_arguments{bytesowner=1[(koinos.btype)=ADDRESS];bytesspender=2[(koinos.btype)=ADDRESS];uint64value=3[jstype=JS_STRING];}messageapprove_result{}messageburn_event{bytesfrom=1[(koinos.btype)=ADDRESS];uint64value=2[jstype=JS_STRING];}messagemint_event{bytesto=1[(koinos.btype)=ADDRESS];uint64value=2[jstype=JS_STRING];}messagetransfer_event{bytesfrom=1[(koinos.btype)=ADDRESS];bytesto=2[(koinos.btype)=ADDRESS];uint64value=3[jstype=JS_STRING];stringmemo=4;}messageapprove_event{bytesowner=1[(koinos.btype)=ADDRESS];bytesspender=2[(koinos.btype)=ADDRESS];uint64value=3[jstype=JS_STRING];}
Now that we have our Protobuf definition, let's generate the derived AssemblyScript code.
koinos-sdk-as-cligenerate-contract-proto
Generating Contract AS proto files...
yarn protoc --plugin=protoc-gen-as=./node_modules/.bin/as-proto-gen --as_out=. assembly/proto/*.proto
yarn run v1.22.19
warning ../../../../../package.json: No license field
$ ../token-tutorial/token/node_modules/.bin/protoc --plugin=protoc-gen-as=./node_modules/.bin/as-proto-gen --as_out=. assembly/proto/token.proto
Done in 0.78s.
Note that after executing this command, the file assembly/proto/token.ts was automatically generated and contains code that will assist us in serializing and deserializing data in and out of the KVM.
Now, we have defined the data in which arguments come into our contract and also the data which is returned by our contract. All we need now is our implementation.
Let's use the tools to automatically generate the boilerplate implementation.
koinos-sdk-as-cligenerate-contract-astoken.proto
Generating boilerplate.ts and index.ts files...
yarn protoc --plugin=protoc-gen-as=./node_modules/.bin/koinos-as-gen --as_out=assembly/ assembly/proto/token.proto
yarn run v1.22.19
warning ../../../../../package.json: No license field
$ /Users/$USER/Workspace/token/token/node_modules/.bin/protoc --plugin=protoc-gen-as=./node_modules/.bin/koinos-as-gen --as_out=assembly/ assembly/proto/token.proto
Done in 1.31s.
After invoking this command, we have two newly generated files. The first is assembly/index.ts which is the first location our smart contract codes begin executing. This file acts as a router to call the appropriate method on our class because of how the smart contract was invoked.
The second file that was generated is assembly/Token.boilerplate.ts. This is where our implementation lives. We should rename this file to Token.ts and begin our implementation.
import{Arrays,authority,chain,error,Protobuf,Storage,System}from"@koinos/sdk-as";import{token}from"./proto/token";constSUPPLY_SPACE_ID=0;constBALANCES_SPACE_ID=1;constALLOWANCES_SPACE_ID=2;exportclassToken{_name:string="[token name]";_symbol:string="[token symbol]";_decimals:u32=8;supply:Storage.Obj<token.balance_object>=newStorage.Obj(System.getContractId(),SUPPLY_SPACE_ID,token.balance_object.decode,token.balance_object.encode,()=>newtoken.balance_object(),true);balances:Storage.Map<Uint8Array,token.balance_object>=newStorage.Map(System.getContractId(),BALANCES_SPACE_ID,token.balance_object.decode,token.balance_object.encode,()=>newtoken.balance_object(),true);allowances:Storage.Map<Uint8Array,token.balance_object>=newStorage.Map(System.getContractId(),ALLOWANCES_SPACE_ID,token.balance_object.decode,token.balance_object.encode,()=>newtoken.balance_object(),true);name(args:token.name_arguments):token.name_result{returnnewtoken.name_result(this._name);}symbol(args:token.symbol_arguments):token.symbol_result{returnnewtoken.symbol_result(this._symbol);}decimals(args:token.decimals_arguments):token.decimals_result{returnnewtoken.decimals_result(this._decimals);}get_info(args:token.get_info_arguments):token.get_info_result{returnnewtoken.get_info_result(this._name,this._symbol,this._decimals);}total_supply(args:token.total_supply_arguments):token.total_supply_result{returnnewtoken.total_supply_result(this.supply.get()!.value);}balance_of(args:token.balance_of_arguments):token.balance_of_result{returnnewtoken.balance_of_result(this.balances.get(args.owner!)!.value);}allowance(args:token.allowance_arguments):token.allowance_result{System.require(args.owner!=null,"account 'owner' cannot be null");System.require(args.spender!=null,"account 'spender' cannot be null");constkey=newUint8Array(50);key.set(args.owner!,0);key.set(args.spender!,25);returnnewtoken.allowance_result(this.allowances.get(key)!.value);}get_allowances(args:token.get_allowances_arguments):token.get_allowances_result{System.require(args.owner!=null,"account 'owner' cannot be null");letkey=newUint8Array(50);key.set(args.owner!,0);key.set(args.start?args.start!:newUint8Array(0),25);letresult=newtoken.get_allowances_result(args.owner,[]);for(leti=0;i<args.limit;i++){constnextAllowance=args.descending?this.allowances.getPrev(key):this.allowances.getNext(key);if(!nextAllowance){break;}if(!Arrays.equal(args.owner,nextAllowance.key!.slice(0,25))){break;}result.allowances.push(newtoken.spender_value(nextAllowance.key!.slice(25),nextAllowance.value.value));key=nextAllowance.key!;}returnresult;}transfer(args:token.transfer_arguments):token.transfer_result{System.require(args.to!=null,"account 'to' cannot be null");System.require(args.from!=null,"account 'from' cannot be null");System.require(!Arrays.equal(args.from,args.to),'cannot transfer to yourself');System.require(this._check_authority(args.from!,args.value),"account 'from' has not authorized transfer",error.error_code.authorization_failure);letfromBalance=this.balances.get(args.from!)!;System.require(fromBalance.value>=args.value,"account 'from' has insufficient balance",error.error_code.failure);lettoBalance=this.balances.get(args.to!)!;fromBalance.value-=args.value;toBalance.value+=args.value;this.balances.put(args.from!,fromBalance);this.balances.put(args.to!,toBalance);System.event('token.transfer_event',Protobuf.encode(newtoken.transfer_event(args.from,args.to,args.value,args.memo),token.transfer_event.encode),[args.to!,args.from!]);returnnewtoken.transfer_result();}mint(args:token.mint_arguments):token.mint_result{System.require(args.to!=null,"account 'to' cannot be null");System.require(args.value!=0,"account 'value' cannot be zero");System.requireAuthority(authority.authorization_type.contract_call,System.getContractId());letsupply=this.supply.get()!;System.require(supply.value<=u64.MAX_VALUE-args.value,'mint would overflow supply',error.error_code.failure);letbalance=this.balances.get(args.to!)!;supply.value+=args.value;balance.value+=args.value;this.supply.put(supply);this.balances.put(args.to!,balance);System.event('token.mint_event',Protobuf.encode(newtoken.mint_event(args.to,args.value),token.mint_event.encode),[args.to!]);returnnewtoken.mint_result();}burn(args:token.burn_arguments):token.burn_result{System.require(args.from!=null,"account 'from' cannot be null");System.require(this._check_authority(args.from!,args.value),"account 'from' has not authorized burn",error.error_code.authorization_failure);letfromBalance=this.balances.get(args.from!)!;System.require(fromBalance.value>=args.value,"account 'from' has insufficient balance",error.error_code.failure);letsupply=this.supply.get()!;System.require(supply.value>=args.value,'burn would underflow supply',error.error_code.failure);supply.value-=args.value;fromBalance.value-=args.value;this.supply.put(supply);this.balances.put(args.from!,fromBalance);System.event('token.burn_event',Protobuf.encode(newtoken.burn_event(args.from,args.value),token.burn_event.encode),[args.from!]);returnnewtoken.burn_result();}approve(args:token.approve_arguments):token.approve_result{System.require(args.owner!=null,"account 'owner' cannot be null");System.require(args.spender!=null,"account 'spender' cannot be null");System.requireAuthority(authority.authorization_type.contract_call,args.owner!);constkey=newUint8Array(50);key.set(args.owner!,0);key.set(args.spender!,25);this.allowances.put(key,newtoken.balance_object(args.value));System.event("token.approve_event",Protobuf.encode(newtoken.approve_event(args.owner,args.spender,args.value),token.approve_event.encode),[args.owner!,args.spender!]);returnnewtoken.approve_result();}_check_authority(account:Uint8Array,amount:u64):bool{constcaller=System.getCaller().caller;if(caller&&caller.length>0){letkey=newUint8Array(50);key.set(account,0);key.set(caller,25);constallowance=this.allowances.get(key)!;if(allowance.value>=amount){allowance.value-=amount;this.allowances.put(key,allowance);returntrue;}}returnSystem.checkAccountAuthority(account);}}
import{Base58,MockVM,authority,Arrays,chain,Protobuf,System,protocol}from"@koinos/sdk-as";import{Token}from"../Token";import{token}from"../proto/token";constCONTRACT_ID=Base58.decode("1DQzuCcTKacbs9GGScRTU1Hc8BsyARTPqe");constMOCK_ACCT1=Base58.decode("1DQzuCcTKacbs9GGScRTU1Hc8BsyARTPqG");constMOCK_ACCT2=Base58.decode("1DQzuCcTKacbs9GGScRTU1Hc8BsyARTPqK");constMOCK_ACCT3=Base58.decode("1DQzuCcTKacbs9GGScRTU1Hc8BsyARTPqP");constheadBlock=newprotocol.block(newUint8Array(0),newprotocol.block_header(newUint8Array(0),10));describe("token",()=>{beforeEach(()=>{MockVM.reset();MockVM.setContractId(CONTRACT_ID);MockVM.setContractArguments(CONTRACT_ID);// Dummy valueMockVM.setEntryPoint(0);MockVM.setCaller(newchain.caller_data(newUint8Array(0),chain.privilege.user_mode));MockVM.setBlock(headBlock);MockVM.setContractMetadata(newchain.contract_metadata_object(newUint8Array(0),false,false,false,false));MockVM.setHeadInfo(newchain.head_info(null,0,1));System.resetCache();});it("should get the name",()=>{consttokenContract=newToken();constres=tokenContract.name(newtoken.name_arguments());expect(res.value).toBe("[token name]");});it("should get the symbol",()=>{consttokenContract=newToken();constres=tokenContract.symbol(newtoken.symbol_arguments());expect(res.value).toBe("[token symbol]");});it("should get the decimals",()=>{consttokenContract=newToken();constres=tokenContract.decimals(newtoken.decimals_arguments());expect(res.value).toBe(8);});it("should get token info",()=>{consttokenContract=newToken();constres=tokenContract.get_info(newtoken.get_info_arguments());expect(res.name).toBe("[token name]");expect(res.symbol).toBe("[token symbol]");expect(res.decimals).toBe(8);});it("should/not burn tokens",()=>{consttokenContract=newToken();letcallerData=newchain.caller_data();callerData.caller=CONTRACT_ID;callerData.caller_privilege=chain.privilege.user_mode;MockVM.setCaller(callerData);// set contract_call authority for CONTRACT_ID to true so that we can mint tokensletauth=newMockVM.MockAuthority(authority.authorization_type.contract_call,CONTRACT_ID,true);MockVM.setAuthorities([auth]);// check total supplylettotalSupplyRes=tokenContract.total_supply(newtoken.total_supply_arguments());expect(totalSupplyRes.value).toBe(0);// mint tokensconstmintArgs=newtoken.mint_arguments(MOCK_ACCT1,123);tokenContract.mint(mintArgs);totalSupplyRes=tokenContract.total_supply(newtoken.total_supply_arguments());expect(totalSupplyRes.value).toBe(123);letbalanceArgs=newtoken.balance_of_arguments(MOCK_ACCT1);letbalanceRes=tokenContract.balance_of(balanceArgs);expect(balanceRes.value).toBe(123);auth=newMockVM.MockAuthority(authority.authorization_type.contract_call,MOCK_ACCT1,true);MockVM.setAuthorities([auth]);callerData.caller=newUint8Array(0);MockVM.setCaller(callerData);// burn tokenstokenContract.burn(newtoken.burn_arguments(MOCK_ACCT1,10));// check eventsconstevents=MockVM.getEvents();expect(events.length).toBe(2);expect(events[0].name).toBe('token.mint_event');expect(events[0].impacted.length).toBe(1);expect(Arrays.equal(events[0].impacted[0],MOCK_ACCT1)).toBe(true);expect(events[1].name).toBe('token.burn_event');expect(events[1].impacted.length).toBe(1);expect(Arrays.equal(events[1].impacted[0],MOCK_ACCT1)).toBe(true);constburnEvent=Protobuf.decode<token.burn_event>(events[1].data,token.burn_event.decode);expect(Arrays.equal(burnEvent.from,MOCK_ACCT1)).toBe(true);expect(burnEvent.value).toBe(10);// check balancebalanceArgs=newtoken.balance_of_arguments(MOCK_ACCT1);balanceRes=tokenContract.balance_of(balanceArgs);expect(balanceRes.value).toBe(113);// check total supplytotalSupplyRes=tokenContract.total_supply(newtoken.total_supply_arguments());expect(totalSupplyRes.value).toBe(113);// save the MockVM state because the burn is going to revert the transactionMockVM.commitTransaction();// does not burn tokensexpect(()=>{consttokenContract=newToken();constburnArgs=newtoken.burn_arguments(MOCK_ACCT1,200);tokenContract.burn(burnArgs);}).toThrow();// check error messageexpect(MockVM.getErrorMessage()).toBe("account 'from' has insufficient balance");MockVM.setAuthorities([]);callerData.caller_privilege=chain.privilege.user_mode;MockVM.setCaller(callerData);// save the MockVM state because the burn is going to revert the transactionMockVM.commitTransaction();expect(()=>{// try to burn tokensconstkoinContract=newToken();constburnArgs=newtoken.burn_arguments(MOCK_ACCT1,123);koinContract.burn(burnArgs);}).toThrow();// check error messageexpect(MockVM.getErrorMessage()).toBe("account 'from' has not authorized burn");// check balancebalanceArgs=newtoken.balance_of_arguments(MOCK_ACCT1);balanceRes=tokenContract.balance_of(balanceArgs);expect(balanceRes.value).toBe(113);// check total supplytotalSupplyRes=tokenContract.total_supply(newtoken.total_supply_arguments());expect(totalSupplyRes.value).toBe(113);});it("should mint tokens",()=>{consttokenContract=newToken();// Contract ID as caller in user modeMockVM.setCaller(newchain.caller_data(CONTRACT_ID,chain.privilege.user_mode));// check total supplylettotalSupplyRes=tokenContract.total_supply(newtoken.total_supply_arguments());expect(totalSupplyRes.value).toBe(0);// mint tokensconstmintArgs=newtoken.mint_arguments(MOCK_ACCT1,123);tokenContract.mint(mintArgs);// check eventsconstevents=MockVM.getEvents();expect(events.length).toBe(1);expect(events[0].name).toBe('token.mint_event');expect(events[0].impacted.length).toBe(1);expect(Arrays.equal(events[0].impacted[0],MOCK_ACCT1)).toBe(true);constmintEvent=Protobuf.decode<token.mint_event>(events[0].data,token.mint_event.decode);expect(Arrays.equal(mintEvent.to,MOCK_ACCT1)).toBe(true);expect(mintEvent.value).toBe(123);// check balanceconstbalanceArgs=newtoken.balance_of_arguments(MOCK_ACCT1);constbalanceRes=tokenContract.balance_of(balanceArgs);expect(balanceRes.value).toBe(123);// check total supplytotalSupplyRes=tokenContract.total_supply(newtoken.total_supply_arguments());expect(totalSupplyRes.value).toBe(123);});it("should not mint tokens if not contract account",()=>{consttokenContract=newToken();// set contract_call authority for MOCK_ACCT1 to true so that we cannot mint tokensconstauth=newMockVM.MockAuthority(authority.authorization_type.contract_call,MOCK_ACCT1,true);MockVM.setAuthorities([auth]);// check total supplylettotalSupplyRes=tokenContract.total_supply(newtoken.total_supply_arguments());expect(totalSupplyRes.value).toBe(0);// check balanceconstbalanceArgs=newtoken.balance_of_arguments(MOCK_ACCT1);letbalanceRes=tokenContract.balance_of(balanceArgs);expect(balanceRes.value).toBe(0);// save the MockVM state because the mint is going to revert the transactionMockVM.commitTransaction();expect(()=>{// try to mint tokensconsttokenContract=newToken();constmintArgs=newtoken.mint_arguments(MOCK_ACCT2,123);tokenContract.mint(mintArgs);}).toThrow();// check balancebalanceRes=tokenContract.balance_of(balanceArgs);expect(balanceRes.value).toBe(0);// check total supplyexpect(totalSupplyRes.value).toBe(0);});it("should not mint tokens if new total supply overflows",()=>{consttokenContract=newToken();// set kernel modeMockVM.setCaller(newchain.caller_data(newUint8Array(0),chain.privilege.kernel_mode));// set contract_call authority for CONTRACT_ID to true so that we can mint tokensconstauth=newMockVM.MockAuthority(authority.authorization_type.contract_call,CONTRACT_ID,true);MockVM.setAuthorities([auth]);letmintArgs=newtoken.mint_arguments(MOCK_ACCT2,123);tokenContract.mint(mintArgs);// check total supplylettotalSupplyRes=tokenContract.total_supply(newtoken.total_supply_arguments());expect(totalSupplyRes.value).toBe(123);// save the MockVM state because the mint is going to revert the transactionMockVM.commitTransaction();expect(()=>{consttokenContract=newToken();constmintArgs=newtoken.mint_arguments(MOCK_ACCT2,u64.MAX_VALUE);tokenContract.mint(mintArgs);}).toThrow();// check total supplytotalSupplyRes=tokenContract.total_supply(newtoken.total_supply_arguments());expect(totalSupplyRes.value).toBe(123);// check error messageexpect(MockVM.getErrorMessage()).toBe("mint would overflow supply");});it("should transfer tokens",()=>{consttokenContract=newToken();// set kernel modeMockVM.setCaller(newchain.caller_data(newUint8Array(0),chain.privilege.kernel_mode));// set contract_call authority for CONTRACT_ID to true so that we can mint tokensconstauthContractId=newMockVM.MockAuthority(authority.authorization_type.contract_call,CONTRACT_ID,true);// set contract_call authority for MOCK_ACCT1 to true so that we can transfer tokensconstauthMockAcct1=newMockVM.MockAuthority(authority.authorization_type.contract_call,MOCK_ACCT1,true);MockVM.setAuthorities([authContractId,authMockAcct1]);// mint tokensconstmintArgs=newtoken.mint_arguments(MOCK_ACCT1,123);tokenContract.mint(mintArgs);// transfer tokensconsttransferArgs=newtoken.transfer_arguments(MOCK_ACCT1,MOCK_ACCT2,10);tokenContract.transfer(transferArgs);// check balancesletbalanceArgs=newtoken.balance_of_arguments(MOCK_ACCT1);letbalanceRes=tokenContract.balance_of(balanceArgs);expect(balanceRes.value).toBe(113);balanceArgs=newtoken.balance_of_arguments(MOCK_ACCT2);balanceRes=tokenContract.balance_of(balanceArgs);expect(balanceRes.value).toBe(10);// check eventsconstevents=MockVM.getEvents();// 2 events, 1st one is the mint event, the second one is the transfer eventexpect(events.length).toBe(2);expect(events[1].name).toBe('token.transfer_event');expect(events[1].impacted.length).toBe(2);expect(Arrays.equal(events[1].impacted[0],MOCK_ACCT2)).toBe(true);expect(Arrays.equal(events[1].impacted[1],MOCK_ACCT1)).toBe(true);consttransferEvent=Protobuf.decode<token.transfer_event>(events[1].data,token.transfer_event.decode);expect(Arrays.equal(transferEvent.from,MOCK_ACCT1)).toBe(true);expect(Arrays.equal(transferEvent.to,MOCK_ACCT2)).toBe(true);expect(transferEvent.value).toBe(10);});it("should not transfer tokens without the proper authorizations",()=>{consttokenContract=newToken();// set kernel modeMockVM.setCaller(newchain.caller_data(newUint8Array(0),chain.privilege.kernel_mode));// set contract_call authority for CONTRACT_ID to true so that we can mint tokensconstauthContractId=newMockVM.MockAuthority(authority.authorization_type.contract_call,CONTRACT_ID,true);// do not set authority for MOCK_ACCT1MockVM.setAuthorities([authContractId]);// mint tokensconstmintArgs=newtoken.mint_arguments(MOCK_ACCT1,123);tokenContract.mint(mintArgs);// save the MockVM state because the transfer is going to revert the transactionMockVM.commitTransaction();expect(()=>{// try to transfer tokens without the proper authorizations for MOCK_ACCT1consttokenContract=newToken();consttransferArgs=newtoken.transfer_arguments(MOCK_ACCT1,MOCK_ACCT2,10);tokenContract.transfer(transferArgs);}).toThrow();// check balancesletbalanceArgs=newtoken.balance_of_arguments(MOCK_ACCT1);letbalanceRes=tokenContract.balance_of(balanceArgs);expect(balanceRes.value).toBe(123);balanceArgs=newtoken.balance_of_arguments(MOCK_ACCT2);balanceRes=tokenContract.balance_of(balanceArgs);expect(balanceRes.value).toBe(0);});it("should not transfer tokens to self",()=>{consttokenContract=newToken();// set kernel modeMockVM.setCaller(newchain.caller_data(newUint8Array(0),chain.privilege.kernel_mode));// set contract_call authority for CONTRACT_ID to true so that we can mint tokensconstauthContractId=newMockVM.MockAuthority(authority.authorization_type.contract_call,CONTRACT_ID,true);// set contract_call authority for MOCK_ACCT1 to true so that we can transfer tokensconstauthMockAcct1=newMockVM.MockAuthority(authority.authorization_type.contract_call,MOCK_ACCT1,true);MockVM.setAuthorities([authContractId,authMockAcct1]);// mint tokensconstmintArgs=newtoken.mint_arguments(MOCK_ACCT1,123);tokenContract.mint(mintArgs);// save the MockVM state because the transfer is going to revert the transactionMockVM.commitTransaction();// try to transfer tokensexpect(()=>{consttokenContract=newToken();consttransferArgs=newtoken.transfer_arguments(Base58.decode("1DQzuCcTKacbs9GGScRTU1Hc8BsyARTPqG"),MOCK_ACCT1,10);tokenContract.transfer(transferArgs);}).toThrow();// check balancesletbalanceArgs=newtoken.balance_of_arguments(MOCK_ACCT1);letbalanceRes=tokenContract.balance_of(balanceArgs);expect(balanceRes.value).toBe(123);// check error messageexpect(MockVM.getErrorMessage()).toBe('cannot transfer to yourself');});it("should not transfer if insufficient balance",()=>{consttokenContract=newToken();// set kernel modeMockVM.setCaller(newchain.caller_data(newUint8Array(0),chain.privilege.kernel_mode));// set contract_call authority for CONTRACT_ID to true so that we can mint tokensconstauthContractId=newMockVM.MockAuthority(authority.authorization_type.contract_call,CONTRACT_ID,true);// set contract_call authority for MOCK_ACCT1 to true so that we can transfer tokensconstauthMockAcct1=newMockVM.MockAuthority(authority.authorization_type.contract_call,MOCK_ACCT1,true);MockVM.setAuthorities([authContractId,authMockAcct1]);// mint tokensconstmintArgs=newtoken.mint_arguments(MOCK_ACCT1,123);tokenContract.mint(mintArgs);// save the MockVM state because the transfer is going to revert the transactionMockVM.commitTransaction();// try to transfer tokensexpect(()=>{consttokenContract=newToken();consttransferArgs=newtoken.transfer_arguments(MOCK_ACCT1,MOCK_ACCT2,456);tokenContract.transfer(transferArgs);}).toThrow();// check balancesletbalanceArgs=newtoken.balance_of_arguments(MOCK_ACCT1);letbalanceRes=tokenContract.balance_of(balanceArgs);expect(balanceRes.value).toBe(123);// check error messageexpect(MockVM.getErrorMessage()).toBe("account 'from' has insufficient balance");});it("should transfer tokens without authority",()=>{consttokenContract=newToken();// set kernel modeMockVM.setCaller(newchain.caller_data(newUint8Array(0),chain.privilege.kernel_mode));// set contract_call authority for CONTRACT_ID to true so that we can mint tokensconstauthContractId=newMockVM.MockAuthority(authority.authorization_type.contract_call,CONTRACT_ID,true);MockVM.setAuthorities([authContractId]);// mint tokensconstmintArgs=newtoken.mint_arguments(MOCK_ACCT1,123);tokenContract.mint(mintArgs);// set caller with MOCK_ACCT1 to allow transfer if the caller is the same fromMockVM.setCaller(newchain.caller_data(MOCK_ACCT1,chain.privilege.kernel_mode));// transfer tokensconsttransferArgs=newtoken.transfer_arguments(MOCK_ACCT1,MOCK_ACCT2,10);tokenContract.transfer(transferArgs);// check balancesletbalanceArgs=newtoken.balance_of_arguments(MOCK_ACCT1);letbalanceRes=tokenContract.balance_of(balanceArgs);expect(balanceRes.value).toBe(113);balanceArgs=newtoken.balance_of_arguments(MOCK_ACCT2);balanceRes=tokenContract.balance_of(balanceArgs);expect(balanceRes.value).toBe(10);// check eventsconstevents=MockVM.getEvents();// 2 events, 1st one is the mint event, the second one is the transfer eventexpect(events.length).toBe(2);expect(events[1].name).toBe('token.transfer_event');expect(events[1].impacted.length).toBe(2);expect(Arrays.equal(events[1].impacted[0],MOCK_ACCT2)).toBe(true);expect(Arrays.equal(events[1].impacted[1],MOCK_ACCT1)).toBe(true);consttransferEvent=Protobuf.decode<token.transfer_event>(events[1].data,token.transfer_event.decode);expect(Arrays.equal(transferEvent.from,MOCK_ACCT1)).toBe(true);expect(Arrays.equal(transferEvent.to,MOCK_ACCT2)).toBe(true);expect(transferEvent.value).toBe(10);});it("should approve",()=>{consttokenContract=newToken();expect(tokenContract.allowance(newtoken.allowance_arguments(MOCK_ACCT1,MOCK_ACCT2)).value).toBe(0);constmockAcc1Auth=newMockVM.MockAuthority(authority.authorization_type.contract_call,MOCK_ACCT1,true);MockVM.setAuthorities([mockAcc1Auth]);tokenContract.approve(newtoken.approve_arguments(MOCK_ACCT1,MOCK_ACCT2,10));expect(tokenContract.allowance(newtoken.allowance_arguments(MOCK_ACCT1,MOCK_ACCT2)).value).toBe(10);MockVM.setAuthorities([mockAcc1Auth]);tokenContract.approve(newtoken.approve_arguments(MOCK_ACCT1,MOCK_ACCT3,20));expect(tokenContract.allowance(newtoken.allowance_arguments(MOCK_ACCT1,MOCK_ACCT3)).value).toBe(20);MockVM.setAuthorities([newMockVM.MockAuthority(authority.authorization_type.contract_call,MOCK_ACCT2,true)]);tokenContract.approve(newtoken.approve_arguments(MOCK_ACCT2,MOCK_ACCT3,30));expect(tokenContract.allowance(newtoken.allowance_arguments(MOCK_ACCT2,MOCK_ACCT3)).value).toBe(30);// check eventsconstevents=MockVM.getEvents();expect(events.length).toBe(3);expect(events[0].name).toBe('token.approve_event');expect(events[0].impacted.length).toBe(2);expect(Arrays.equal(events[0].impacted[0],MOCK_ACCT1)).toBe(true);expect(Arrays.equal(events[0].impacted[1],MOCK_ACCT2)).toBe(true);expect(events[1].name).toBe('token.approve_event');expect(events[1].impacted.length).toBe(2);expect(Arrays.equal(events[1].impacted[0],MOCK_ACCT1)).toBe(true);expect(Arrays.equal(events[1].impacted[1],MOCK_ACCT3)).toBe(true);expect(events[2].name).toBe('token.approve_event');expect(events[2].impacted.length).toBe(2);expect(Arrays.equal(events[2].impacted[0],MOCK_ACCT2)).toBe(true);expect(Arrays.equal(events[2].impacted[1],MOCK_ACCT3)).toBe(true);// Tests basic allowances returnletallowances=tokenContract.get_allowances(newtoken.get_allowances_arguments(MOCK_ACCT1,newUint8Array(0),10));expect(Arrays.equal(allowances.owner,MOCK_ACCT1)).toBe(true);expect(allowances.allowances.length).toBe(2);expect(Arrays.equal(allowances.allowances[0].spender,MOCK_ACCT2)).toBe(true);expect(allowances.allowances[0].value).toBe(10);expect(Arrays.equal(allowances.allowances[1].spender,MOCK_ACCT3)).toBe(true);expect(allowances.allowances[1].value).toBe(20);// Tests allowances descendingallowances=tokenContract.get_allowances(newtoken.get_allowances_arguments(MOCK_ACCT1,MOCK_ACCT3,10,true));expect(Arrays.equal(allowances.owner,MOCK_ACCT1)).toBe(true);expect(allowances.allowances.length).toBe(1);expect(Arrays.equal(allowances.allowances[0].spender,MOCK_ACCT2)).toBe(true);expect(allowances.allowances[0].value).toBe(10);// Tests allowances limitallowances=tokenContract.get_allowances(newtoken.get_allowances_arguments(MOCK_ACCT1,newUint8Array(0),1));expect(Arrays.equal(allowances.owner,MOCK_ACCT1)).toBe(true);expect(allowances.allowances.length).toBe(1);expect(Arrays.equal(allowances.allowances[0].spender,MOCK_ACCT2)).toBe(true);expect(allowances.allowances[0].value).toBe(10);// Tests allowances paginationallowances=tokenContract.get_allowances(newtoken.get_allowances_arguments(MOCK_ACCT1,MOCK_ACCT2,10));expect(Arrays.equal(allowances.owner,MOCK_ACCT1)).toBe(true);expect(allowances.allowances.length).toBe(1);expect(Arrays.equal(allowances.allowances[0].spender,MOCK_ACCT3)).toBe(true);expect(allowances.allowances[0].value).toBe(20);// Tests another owner's allowancesallowances=tokenContract.get_allowances(newtoken.get_allowances_arguments(MOCK_ACCT2,newUint8Array(0),10));expect(Arrays.equal(allowances.owner,MOCK_ACCT2)).toBe(true);expect(allowances.allowances.length).toBe(1);expect(Arrays.equal(allowances.allowances[0].spender,MOCK_ACCT3)).toBe(true);expect(allowances.allowances[0].value).toBe(30);});it("should require an approval",()=>{consttokenContract=newToken();MockVM.setCaller(newchain.caller_data(CONTRACT_ID,chain.privilege.user_mode));tokenContract.mint(newtoken.mint_arguments(MOCK_ACCT1,100));MockVM.setCaller(newchain.caller_data(MOCK_ACCT2,chain.privilege.user_mode));// should not transfer because allowance does not existexpect(()=>{consttokenContract=newToken();tokenContract.transfer(newtoken.transfer_arguments(MOCK_ACCT1,MOCK_ACCT2,10));}).toThrow();expect(MockVM.getErrorMessage()).toBe("account 'from' has not authorized transfer");// create allowance for 20 tokensMockVM.setCaller(newchain.caller_data(newUint8Array(0),chain.privilege.kernel_mode));MockVM.setAuthorities([newMockVM.MockAuthority(authority.authorization_type.contract_call,MOCK_ACCT1,true)]);tokenContract.approve(newtoken.approve_arguments(MOCK_ACCT1,MOCK_ACCT2,20));MockVM.setCaller(newchain.caller_data(MOCK_ACCT2,chain.privilege.user_mode));// should not transfer because allowance is too smallexpect(()=>{consttokenContract=newToken();tokenContract.transfer(newtoken.transfer_arguments(MOCK_ACCT1,MOCK_ACCT2,25));}).toThrow();// should transfer partial amount of allowancetokenContract.transfer(newtoken.transfer_arguments(MOCK_ACCT1,MOCK_ACCT2,10));expect(tokenContract.balance_of(newtoken.balance_of_arguments(MOCK_ACCT1)).value).toBe(90);expect(tokenContract.balance_of(newtoken.balance_of_arguments(MOCK_ACCT2)).value).toBe(10);expect(tokenContract.allowance(newtoken.allowance_arguments(MOCK_ACCT1,MOCK_ACCT2)).value).toBe(10);});});
In the highlighted lines we changed the [token name] variable to My Token Name and the symbol variable to MTN.
Keep in mind that since this token name and symbol have already been minted to the testnet you should replace your own name and symbol with the ones used in this guide.
Let's run the tests again and see what happens.
koinos-sdk-as-clirun-tests
Running tests...
yarn asp --verbose --config as-pect.config.js
$ /home/sgerbino/Workspace/token/node_modules/.bin/asp --verbose --config as-pect.config.js
___ _____ __
/ | / ___/ ____ ___ _____/ /_
/ /| | \__ \______/ __ \/ _ \/ ___/ __/
/ ___ |___/ /_____/ /_/ / __/ /__/ /_
/_/ |_/____/ / .___/\___/\___/\__/
/_/
⚡AS-pect⚡ Test suite runner [8.1.0]
Using config: /home/sgerbino/Workspace/token/as-pect.config.js
ASC Version: 0.27.29
[Log]Using code coverage: assembly/*.ts
[Log]Using coverage: assembly/*.ts
[Describe]: token
[Fail]: ✖ should get the name
[Actual]: "My Token Name"
[Expected]: "[token name]"
[Stack]: RuntimeError: unreachable
at node_modules/@as-pect/assembly/assembly/internal/assert/assert (wasm://wasm/0012d0ca:wasm-function[319]:0x2ac3)
at node_modules/@as-pect/assembly/assembly/internal/Expectation/Expectation<~lib/string/String|null>#toBe (wasm://wasm/0012d0ca:wasm-function[938]:0x120a3)
at start:assembly/__tests__/Token.spec~anonymous|0~anonymous|1 (wasm://wasm/0012d0ca:wasm-function[939]:0x12156)
at node_modules/@as-pect/assembly/assembly/internal/call/__call (wasm://wasm/0012d0ca:wasm-function[561]:0x37e5)
at export:node_modules/@as-pect/assembly/assembly/internal/call/__call (wasm://wasm/0012d0ca:wasm-function[1267]:0x24d50)
[Fail]: ✖ should get the symbol
[Actual]: "MTN"
[Expected]: "[token symbol]"
[Stack]: RuntimeError: unreachable
at node_modules/@as-pect/assembly/assembly/internal/assert/assert (wasm://wasm/0012d0ca:wasm-function[319]:0x2ac3)
at node_modules/@as-pect/assembly/assembly/internal/Expectation/Expectation<~lib/string/String|null>#toBe (wasm://wasm/0012d0ca:wasm-function[938]:0x120a3)
at start:assembly/__tests__/Token.spec~anonymous|0~anonymous|2 (wasm://wasm/0012d0ca:wasm-function[942]:0x122e3)
at node_modules/@as-pect/assembly/assembly/internal/call/__call (wasm://wasm/0012d0ca:wasm-function[561]:0x37e5)
at export:node_modules/@as-pect/assembly/assembly/internal/call/__call (wasm://wasm/0012d0ca:wasm-function[1267]:0x24d50)
[Success]: ✔ should get the decimals
[Fail]: ✖ should get token info
[Actual]: "My Token Name"
[Expected]: "[token name]"
[Stack]: RuntimeError: unreachable
at node_modules/@as-pect/assembly/assembly/internal/assert/assert (wasm://wasm/0012d0ca:wasm-function[319]:0x2ac3)
at node_modules/@as-pect/assembly/assembly/internal/Expectation/Expectation<~lib/string/String|null>#toBe (wasm://wasm/0012d0ca:wasm-function[938]:0x120a3)
at start:assembly/__tests__/Token.spec~anonymous|0~anonymous|4 (wasm://wasm/0012d0ca:wasm-function[951]:0x127e5)
at node_modules/@as-pect/assembly/assembly/internal/call/__call (wasm://wasm/0012d0ca:wasm-function[561]:0x37e5)
at export:node_modules/@as-pect/assembly/assembly/internal/call/__call (wasm://wasm/0012d0ca:wasm-function[1267]:0x24d50)
We now get these errors because we haven't updated the tests to reflect the changes we made to the token. Let's update the tests to reflect the changes we made to the token.
In the following code snippets the highlighted lines were added.
it("should get the name",()=>{consttokenContract=newToken();constres=tokenContract.name(newtoken.name_arguments());expect(res.value).toBe("My Token Name");});it("should get the symbol",()=>{consttokenContract=newToken();constres=tokenContract.symbol(newtoken.symbol_arguments());expect(res.value).toBe("MTN");});it("should get the decimals",()=>{consttokenContract=newToken();constres=tokenContract.decimals(newtoken.decimals_arguments());expect(res.value).toBe(8);});it("should get token info",()=>{consttokenContract=newToken();constres=tokenContract.get_info(newtoken.get_info_arguments());expect(res.name).toBe("My Token Name");expect(res.symbol).toBe("MTN");expect(res.decimals).toBe(8);});
If we run the tests again we should now have an all green 100% pass.
koinos-sdk-as-clirun-tests
Running tests...
yarn asp --verbose --config as-pect.config.js
$ /home/sgerbino/Workspace/token/node_modules/.bin/asp --verbose --config as-pect.config.js
___ _____ __
/ | / ___/ ____ ___ _____/ /_
/ /| | \__ \______/ __ \/ _ \/ ___/ __/
/ ___ |___/ /_____/ /_/ / __/ /__/ /_
/_/ |_/____/ / .___/\___/\___/\__/
/_/
⚡AS-pect⚡ Test suite runner [8.1.0]
Using config: /home/sgerbino/Workspace/token/as-pect.config.js
ASC Version: 0.27.29
[Log]Using code coverage: assembly/*.ts
[Log]Using coverage: assembly/*.ts
[Describe]: token
[Success]: ✔ should get the name
[Success]: ✔ should get the symbol
[Success]: ✔ should get the decimals
[Success]: ✔ should get token info
[Event] token.mint_event / [ '1DQzuCcTKacbs9GGScRTU1Hc8BsyARTPqG' ] / ChkAiCtotTmdMTyQZ/OEFgfmKMEtVCA8jEc3EHs=
[Event] token.burn_event / [ '1DQzuCcTKacbs9GGScRTU1Hc8BsyARTPqG' ] / ChkAiCtotTmdMTyQZ/OEFgfmKMEtVCA8jEc3EAo=
[Contract Exit] account 'from' has insufficient balance
[Contract Exit] account 'from' has not authorized burn
[Success]: ✔ should/not burn tokens
[Event] token.mint_event / [ '1DQzuCcTKacbs9GGScRTU1Hc8BsyARTPqG' ] / ChkAiCtotTmdMTyQZ/OEFgfmKMEtVCA8jEc3EHs=
[Success]: ✔ should mint tokens
[Contract Exit] account '1DQzuCcTKacbs9GGScRTU1Hc8BsyARTPqe' authorization failed
[Success]: ✔ should not mint tokens if not contract account
[Event] token.mint_event / [ '1DQzuCcTKacbs9GGScRTU1Hc8BsyARTPqK' ] / ChkAiCtotTmdMTyQZ/OEFgfmKMEtVCA8jEc6EHs=
[Contract Exit] mint would overflow supply
[Success]: ✔ should not mint tokens if new total supply overflows
[Event] token.mint_event / [ '1DQzuCcTKacbs9GGScRTU1Hc8BsyARTPqG' ] / ChkAiCtotTmdMTyQZ/OEFgfmKMEtVCA8jEc3EHs=
[Event] token.transfer_event / [
'1DQzuCcTKacbs9GGScRTU1Hc8BsyARTPqK',
'1DQzuCcTKacbs9GGScRTU1Hc8BsyARTPqG'
] / ChkAiCtotTmdMTyQZ/OEFgfmKMEtVCA8jEc3EhkAiCtotTmdMTyQZ/OEFgfmKMEtVCA8jEc6GAo=
[Success]: ✔ should transfer tokens
[Event] token.mint_event / [ '1DQzuCcTKacbs9GGScRTU1Hc8BsyARTPqG' ] / ChkAiCtotTmdMTyQZ/OEFgfmKMEtVCA8jEc3EHs=
[Contract Exit] account 'from' has not authorized transfer
[Success]: ✔ should not transfer tokens without the proper authorizations
[Event] token.mint_event / [ '1DQzuCcTKacbs9GGScRTU1Hc8BsyARTPqG' ] / ChkAiCtotTmdMTyQZ/OEFgfmKMEtVCA8jEc3EHs=
[Contract Exit] cannot transfer to yourself
[Success]: ✔ should not transfer tokens to self
[Event] token.mint_event / [ '1DQzuCcTKacbs9GGScRTU1Hc8BsyARTPqG' ] / ChkAiCtotTmdMTyQZ/OEFgfmKMEtVCA8jEc3EHs=
[Contract Exit] account 'from' has insufficient balance
[Success]: ✔ should not transfer if insufficient balance
[Event] token.mint_event / [ '1DQzuCcTKacbs9GGScRTU1Hc8BsyARTPqG' ] / ChkAiCtotTmdMTyQZ/OEFgfmKMEtVCA8jEc3EHs=
[Event] token.transfer_event / [
'1DQzuCcTKacbs9GGScRTU1Hc8BsyARTPqK',
'1DQzuCcTKacbs9GGScRTU1Hc8BsyARTPqG'
] / ChkAiCtotTmdMTyQZ/OEFgfmKMEtVCA8jEc3EhkAiCtotTmdMTyQZ/OEFgfmKMEtVCA8jEc6GAo=
[Success]: ✔ should transfer tokens without authority
[Event] token.approve_event / [
'1DQzuCcTKacbs9GGScRTU1Hc8BsyARTPqG',
'1DQzuCcTKacbs9GGScRTU1Hc8BsyARTPqK'
] / ChkAiCtotTmdMTyQZ/OEFgfmKMEtVCA8jEc3EhkAiCtotTmdMTyQZ/OEFgfmKMEtVCA8jEc6GAo=
[Event] token.approve_event / [
'1DQzuCcTKacbs9GGScRTU1Hc8BsyARTPqG',
'1DQzuCcTKacbs9GGScRTU1Hc8BsyARTPqP'
] / ChkAiCtotTmdMTyQZ/OEFgfmKMEtVCA8jEc3EhkAiCtotTmdMTyQZ/OEFgfmKMEtVCA8jEc+GBQ=
[Event] token.approve_event / [
'1DQzuCcTKacbs9GGScRTU1Hc8BsyARTPqK',
'1DQzuCcTKacbs9GGScRTU1Hc8BsyARTPqP'
] / ChkAiCtotTmdMTyQZ/OEFgfmKMEtVCA8jEc6EhkAiCtotTmdMTyQZ/OEFgfmKMEtVCA8jEc+GB4=
[Success]: ✔ should approve
[Event] token.mint_event / [ '1DQzuCcTKacbs9GGScRTU1Hc8BsyARTPqG' ] / ChkAiCtotTmdMTyQZ/OEFgfmKMEtVCA8jEc3EGQ=
[Contract Exit] account 'from' has not authorized transfer
[Event] token.approve_event / [
'1DQzuCcTKacbs9GGScRTU1Hc8BsyARTPqG',
'1DQzuCcTKacbs9GGScRTU1Hc8BsyARTPqK'
] / ChkAiCtotTmdMTyQZ/OEFgfmKMEtVCA8jEc3EhkAiCtotTmdMTyQZ/OEFgfmKMEtVCA8jEc6GBQ=
[Contract Exit] account 'from' has not authorized transfer
[Event] token.transfer_event / [
'1DQzuCcTKacbs9GGScRTU1Hc8BsyARTPqK',
'1DQzuCcTKacbs9GGScRTU1Hc8BsyARTPqG'
] / ChkAiCtotTmdMTyQZ/OEFgfmKMEtVCA8jEc3EhkAiCtotTmdMTyQZ/OEFgfmKMEtVCA8jEc6GAo=
[Success]: ✔ should require an approval
[File]: assembly/__tests__/Token.spec.ts
[Groups]: 2 pass, 2 total
[Result]: ✔ PASS
[Snapshot]: 0 total, 0 added, 0 removed, 0 different
[Summary]: 15 pass, 0 fail, 15 total
[Time]: 419.176ms
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Coverage Report:
┌───────────────────┬───────┬───────┬──────┬───────┬───────────┐
│ File │ Total │ Block │ Func │ Expr │ Uncovered │
├───────────────────┼───────┼───────┼──────┼───────┼───────────┤
│ assembly/Token.ts │ 97.7% │ 100% │ 100% │ 83.3% │ 80:39 │
├───────────────────┼───────┼───────┼──────┼───────┼───────────┤
│ total │ 97.7% │ 100% │ 100% │ 83.3% │ │
└───────────────────┴───────┴───────┴──────┴───────┴───────────┘
[Summary]
[Tests]: 15 / 15
[Groups]: 2 / 2
[Snapshots]: 0 / 0, Added 0, Changed 0
[Result]: ✔ Pass!
Done in 2.12s.
Now that our contract is deployed to the testnet let's register our contract so we can mint tokens by issuing the register command register <name:contract-name> <address:address> [abi-filename:file]. Note that we are registering the address that we uploaded to (see above).
register test 163m4hKj1QHLCyHgnyNPw8TZU5ov25QGQX token.abi
We will get this response:
Contract 'test' at address 163m4hKj1QHLCyHgnyNPw8TZU5ov25QGQX registered
We will now have new commands available to us to interact with our contract.
To make sure it all worked we can try the command test.symbol
test.symbol
value: "MTN"
We can now issue the mint command to mint our tokens. Let's create a new wallet we can mint the tokens to.
create mint_to.wallet password
Created and opened new wallet: mint_to.wallet
Address: 14xHsbnNnHVqDXaHq99A3ZEAEzQAwd9mtt
We need to switch back to our test.wallet so we can mint the tokens.
Since we are using the register command to mint the tokens, the decimals of the token are not taken into consideration. When we created out token contract we did not change the default 8 decimal places (see highlighted).
We therefore need to enter the number of tokens to mint in the smallest unit of the token. In this case, we need to enter 100000000 to mint 1 token. Later, we will use the register_token command which will take the decimals into account.
We should get this response:
Calling test.mint with arguments 'to: "14xHsbnNnHVqDXaHq99A3ZEAEzQAwd9mtt"
value: 100000000000000
'
Transaction with ID 0x1220e558d2d1d634321ce90e452d126a4b6b89723345b4fa9f936017770c0e28bc84 containing 1 operations submitted.
Mana cost: 0.03434252 (Disk: 95, Network: 284, Compute: 514975)
Let's transfer some of the tokens we just minted to a new wallet. We can use the same contract we just registered, however the CLI provides a command, register_token that provides a cleaner interface for interacting with tokens directly.
Open the mint_to.wallet and this time register the token contract using the register_token command.
Transferring 0.00000001 MTN to 1Npov1QbcRUuw17xoWDGKEAfk2X5hJ8ueW
Transaction with ID 0x12202aa89a221ac7be8c4cbc42ff0d4cdf653a52f7b96a5824de826947684e882c37 containing 1 operations submitted.
Mana cost: 0.03231164 (Disk: 95, Network: 308, Compute: 456741)
Note
When using the register_token command as indicated by the documentation in the Koinos CLI: Register token section, the decimals of the token are taken into consideration. As demonstrated above, we are sending 0.00000001 MTN as indicated by the 8 decimal places. If you want to use register_token with an offline wallet, you need to provide the precision and symbol manually.
Congratulations, We successfully deployed our contract to the testnet, minted a million tokens and made a transfer! We can check the block explorer and see that we now have the newly minted tokens in our wallet and that tokens were transferred from our minted wallet.