Skip to content

Instantly share code, notes, and snippets.

@erikside
Created June 4, 2024 16:30
Show Gist options
  • Select an option

  • Save erikside/78ad2689ff6e08a37af57ae85a870b99 to your computer and use it in GitHub Desktop.

Select an option

Save erikside/78ad2689ff6e08a37af57ae85a870b99 to your computer and use it in GitHub Desktop.
Created using remix-ide: Realtime Ethereum Contract Compiler and Runtime. Load this file by pasting this gists URL or ID at https://remix.ethereum.org/#version=soljson-v0.8.26+commit.8a97fa7a.js&optimize=false&runs=200&gist=
[core]
repositoryformatversion = 0
filemode = false
bare = false
logallrefupdates = true
symlinks = false
ignorecase = true
ref: refs/heads/main
DIRC f^��(�<@f^��(�<@ �����o#q{K���Sc��X�W�n.prettierrc.jsonf^��(,l�f^��(,l����}�7i��$���IQ��B.�
README.txtf^��;�f^��;����+��^x;n��G�g�f6�contracts/1_Storage.solf^���(�f^���(�����I����*̛���>K���?�'contracts/2_Owner.solf^�� ח�f^�� ח�������l6 l��8Յ�%��+�Zcontracts/3_Ballot.solf^��!�y�f^��!�y����Q=Imv8�n{s9'/�:pSB�6scripts/deploy_with_ethers.tsf^��"�f^��"����ܠ��ݾ?87G�k�)P�.�scripts/deploy_with_web3.tsf^��$zaf^��$za��ՎJߕ�Q虥E؅.�u�`scripts/ethers-lib.tsf^��%P�f^��%P��� ��􄉵���C�� av�ascripts/web3-lib.tsf^��%�T�f^��%�T����In��ˠ/�ds�9���5��[tests/Ballot_test.solf^��&�x�f^��&�x�����7�Ûv�%��1���$�s��tests/storage.test.js����MF�Dd{|�/cv�s
x�+)JMU047c040031QHI-�ɯ�/�,ɈO-�H-*�+)f���-�X�W]l���jU�4� ����$c��; v��������Q� ��z��!f��d&��yݟ�,���̥�7Z�����H`�*�Sv�ʗ�έ�˞�q�rT�#��j"��F�
x�+)JMU0�0a040031Q�+(J-)�L-*J��*��cش4_���{v?���#���˃� rut�u�+�(axT��<��E��F�w����:��21000PH��+)JL.)fX�,���}���'�m�T�ƵB�'e�30,�o��t�����Yqo�~1� QQ�Z\R���Jqյ�4��O�>�]R|���}�O�
x���A
�0=��e�M�DD<��g�t������/� < s&͵���� ���Ô'����R��;�}�<td���^��&��L ;��.5J٤��9�}?�F�h�w��gf�wQ�~�Wy�r}����#�5o��@�
x��Q[k�0ݳ��O6$��<l��A�` ��X e��k�%O��4�����d��窣\��7/�׫���+U��4�\�a�Q�����j�i4Nnk o�*��~���"ŻT\�4 T�X����ɳ��0C��+�10��l�֒U!��C�T0���Xwjq4��++�Ia���z�#*D���dq�Ajmy�#���W� �|���q��^�/�&��h�G��l'���5+k���:��Q��͵*�t�t߉�hZ_EC`Ҕ��L/�8Ύ�i�0�ZD'f��98�STT��Q�(�] ������F�=Ϊ�?t�)J'���XЯV�脺;���Ze8J���Z� eJzD
_�V�� \�q��C�ucuygg�ar<JF��{��j7��5q��7�[�L��hw�<��ZO����հ\"�<�>�5�
x�M��J�@E}�W\�Є���RhA�i|��f���솝 ZJ�]����0Ý3�Ni}����&����`z�V%a`� ��!-!.��P ��׀����'Ú2�p̢<dz%ńN<i�w���8˯ ���gp��ƨGAI��087^�?���U�C�M�
�5����~p1���"} :r|=���G���@����I�]��eYD��z����"�/�W֔�(J�F�b��)$��� �`[�/ed�%�9���o��[ʬo�UU��7�=]�<9��3��"��߱�6e1��&itN����3
x�ՒOO1�9�S<���ϦBD��R�=P���=�Z;�cJ���'$m�ֲ=��f<���.�.���]��2 ��I ��p8���q�$0ޙ{���$�z`P���@ݔ� �̦�L Y�~^�����K���ȩ�ꍑ�S�g&Jv"���rM9�V?%&Y�#.�/3��V�Y��GO�K)/�� �ɛ!�A��I+�Y�� ��|'#1-�rwW;����A�兏K��u,�������q�jc��@�E�h+&k�ܤ��j�� �$�O������< ���� y�i%]��ƕ�%qa\ʹY������m���R���/��73������ƽ�֟��+kx_O��a�~<��]�U���g<�
x�]S�n�@�9_1Hv��,���B!�D��]���}g�m�FQ��l'./�λ3;3��7>ǫ�o޾�m�Eqk�p�V|�d8&�Evu���p���fT��
�T�о��#�Ǡb]u:�6�2\����
P�22��"txO�psI�ؠ=��.��
�� 6���9v�ks��(���7W��t���h�W�O�\N�H#�;q8~�F}\Νf�+:([���+��}_cHc�\�bn�L�k��}y�;� �~�LZn���q�@�6����a�u^�O���e�xehM]�Bl�p�e�?n?}þ�E P�c!��S���6��;���R�f`�Aa'��a���� � �hB/�;�k�����%�Cvn��3h�w����5��7i�^P�햅��#��i-+���5����1I���d­}25M�lm�?�QŒ��T�_m�����p���͝�G[�$}4��j|N I,� ��\�EO��c������L72�gƛ·,��b���|���^��~-g��.}����ܮ����ʅ/� �W��و�1d8�4�N�Ƙ~�#"��~�(���/����{F� �H�sj���Z�f3�� ��71���;=� ���
x�}T]o�6��~Łf;plC;,��Y0hѢ�� 4y%�H��ʎQ���$[΂�E���:��p���߼}�j��×����h5�H���������/��/�bǪ�bp�X9��z9�e��o�����(l� ,m����>G��hU���Wx/V����wC{<��'��!tТ�� +-m4�(c�bĎ�^I�j���'/(��Op����2�"��ɤ�c��G2�dl����CF��.u0Y H�ޒ�[B+�ac�8ژkL������} �J�Q��j�4d(1f���Lc�^�F ��l\*i<=�n�!����(� ZUsnj%i��$�N���(��N�L`� ���Ɵ[�D�I�\��RT*����j >ΰi>�?ց�ڽd�J!�*kH��M��e�ub�N��6Ё����E�
�tHbt��p ��P.�8�I:�wNy��T��[%8�c8p�չv7ؤ��<�7�X�۳�at{:�D<MW��ڿ?�JnH��N�nh�‘j�I���n���e���B5��yZ�`s��p&00w�z������8�눇�aN*�Ø=3}��{jE���'��b��T��D���dԓ�C��=#b�N�=Zk�Y'�c�l�սwb_2�V`pO<��^d���a���Ü"�E�^�U��� =
�W{K�U"z�� �A�~�� B��
x�+)JMU042`040031Q0�.�/JLO�+��a�+'z)��:o��&����43��P�F���y�E`e3=��v[���}3�yw}j?C��8�)1''�����3�-���W�1I[� [��1,
x����
�0 �=�)B�2ă��[����n٬�f�� c�.+l0cB{J�� ��ɪ�����e)\}�{�Py���l���X<N�r�t���|X��E���RH��/����D\[<s�� �,NV�R�&�3�0<{�;U<������6^p��k���xH�����%�pυ._ݢ6��̢�
x�mS]k�@�~�<$GN�"�iK!�|@�P
^Ik�R��ܭ�[���$�j�@¾�ݙ��M+������;U�����
Kkj�N� ?���b)����u��R�|���(��Q�9Ky���0fgg��W��BJF��X�i��Vd��ΉU�h��#� �~�ҧ��W��YK�k�ۛd �J9iS3���u&Ɔ�`���7BYf�Z�v��T�u��Œv�����z]�l��[�Zu�,��j��N5�j���g����:{� ���q7tB��`��婗~{�nt�& ������^� �7�z���0�损����l͛��Nz��8�L-:�Jx��k-t6ã��$�k.�j%�̹�w��)UVB9�ْp�����z�*j����%ew��7í�h��8�|�p ����!�LRb�Ej�Ʊ�*����g0~uF/�m�Y('!�����1^�uц��r���UU.U���`N,�T���_1�ɱr?���_������ ��>�:�aU����AuL�:Q�]����c>@�n�]m�d�E�?�V839�&}�L�H�X׬�%~~��k�H������v{�vf�nr���i?/~u��į�~�ˏ�ӑ�j� �M�#A����
x�M��j�0�]�?2P骔�bZ{�Q�c��1��H'��>�v�UH�ϗ��x|Z�e���SC
���{*�%u����l��)�������+S㝊5�*�pCd^5 Ohő�{G#$m�)M��6���l����(����zcF���M��)�rث�aH����}oBTζS;�f�dz�j�>�lGu�� ��:�=o��N/�FU�ȃ@��u��m�˕���D�z�UA,�`$����n�N�<Ñ�5c q�g+�杣||&�q�)ն��,y����
����0�/��e����ݦ)m�{Q� .I��*���
x�m��k�@�{޿❪�%��Zz(�����d������ID����K-���}��͏Ir� ��0�z���/UJڑ���f�Udg�Z-�� ��rWH8��L��(x �x��� � z���V��l��Q�dT��#,�UTj�W�!QK�d�w�rl�YF�o+��Ԫ���Å��9n���=Y��B��V�<��I���i� ] �y'�����w��"��RZY4��\�����V:eetW� ����\�8u@��E���>�qeu?�R����b���k_@jEt!����K����{��
x�uTMs�4�_�N�b��.)���L��653���u"K�%��${>²7������'������W�lV���nu��;��y�����v�Ԇ:~CK{=؈��K�uC��^(���k��\�u���h�$�,!"rGP�n�h|��H-F���ܯ@���:��T�3$-�gK��!�;�]�wϖ�z��v��E�ƻ��\�e�&za
�J}�P���&���z�D#4��39v�kH=-���{4��-�q�*�}�"4�}F�=���A�����?z��[?A�UXǤ ��V��5Y?V�B)�������[�R4#��hk},N��6����o�6z�:gVJmo7��6�H牰��%��_i4����ti7_v��u,F����P@�EC��r-:��R�~�Ԍ�%��=���S� ���"��Ցr�1R8��� 6�/����3�D���m�Q��h9 �}�n/-����fQ�f���1��<1<� ����1<)��˗e�������x8���7Fwk4cp�R�������JLDc�y�w3�,��(�\�,ǜ[lW��N]MR���+�$��B��PS�l����!�C��_��Z�'-���7$�t촭�z��C��J�3���z/����O�Ԧb��C��#�R̯�9���`������㟋�zZ��[3��DZU�0j�c��h.�6���P(�I'��5:�;;eˏj�����z;��T��ځD���K�rz͒`u:��5����Ǵ���ңRI"������['�`��*�/�&^
x�+)JMU�0c040031QpJ���/�/I-.�+��a�ɛ����s)�M���0=w0���$�(1=U�6����|���eU%W�4��ڦ���wm�&�
x��Wm��6 ���
��$�朴�0�i�m-��m�ZtA��L�M�<INf���l�I�0}�c���!Em����_=z��|o߼���5͑+��-�k��(�«7�o�d�Ѩ�dWP�т�������-�o��r4�__���[M5C��0&�}Q�n��a�\+� M�j)rT
|�K��@�;���#���r��$����h��lr �F �+��5��J����?U@�F4�i�]���� ��.݂� �@�DC�R �I$E��:UR��⭢��k�wh��il,��� ��t�ζ�O-aV�D������v �h '�����.A ��Ԅr�M�Aiv۴� b+�H��Q�c'�Ș�OO�sbt�-�P5y y��F���KsR�0U)��/`��Ƥ�f䦧�y!�P��Q42*�N@�n6�搗�J�� T��)�M���#�4��Ry��u� ��fy���H4��8�jy)D��q0�3�P���BM$� �js��V�����\p� !'>�wk���M-M#�D��Tj�)��e'�q �3_K+x��7k+$L,dV�X�g�ƙ���Ç��o����,ˎ�1�6�
h�j!�l�J]�͟�k ��Ӫ��Q�$˲�x�H�y��Z\L� /P:@�X��M��2P=Mc���ى\����H>���c�����{��EilÐ-\tI��`?�g-lr˜���Z�]I��{��o���tP��=�jx'L=u��O��ϘĿ*q�D��V���i�~1q� 6 �x�� ��Ԣ�����g��Yg��6}�۲L��7I͆�Y�bzRhSb)� �/��b��t�{6;�Z��iN�p(i��[�[�yx��Io� ��(VZH�CH�{tק�z��������Lz��q����[dۛ�h��Qe����tV%e#-�Y�X��N�a�Ң*֊L��5�!lE� ����r Q�� ���K���k3X�%9:vOqjae'���c�
�X^@4}H���&����L����I���P��s�Z�G�x? �@X]��K=qEm�.<\�����' �)���-,r�|Z�'n{oK�~M��}��~�K��{�-`ByΚ���63��i��7�(:����:3g�ʼn��R�765�) R��|1N�Q!���]$I�)�� �~$
�H��s�d؂���~>��,�.Ψ78:u��%FI����Y�j�e t)�H�EE45gzk� �{����zyi쪞���T�\�E^��n ߌ�ʹ��9�h��O�Z➊FyzRnΤ<7[����Ƀ�0n}�w�����줟�)����p;t�]W��N'��ˣ��Lʵ���hR���tJ6�5,F��ɦCe��8v���j��
�T�x�Y hBSg��v�n��uب�4Ws{������D~�J(�<�� ���ؾ���94 ��h�!IbGbNr��p��㿠�[�
299c2829aeab9275643d818789a4000c72a36549
{
"overrides": [
{
"files": "*.sol",
"options": {
"printWidth": 80,
"tabWidth": 4,
"useTabs": false,
"singleQuote": false,
"bracketSpacing": false
}
},
{
"files": "*.yml",
"options": {}
},
{
"files": "*.yaml",
"options": {}
},
{
"files": "*.toml",
"options": {}
},
{
"files": "*.json",
"options": {}
},
{
"files": "*.js",
"options": {}
},
{
"files": "*.ts",
"options": {}
}
]
}
REMIX DEFAULT WORKSPACE
Remix default workspace is present when:
i. Remix loads for the very first time
ii. A new workspace is created with 'Default' template
iii. There are no files existing in the File Explorer
This workspace contains 3 directories:
1. 'contracts': Holds three contracts with increasing levels of complexity.
2. 'scripts': Contains four typescript files to deploy a contract. It is explained below.
3. 'tests': Contains one Solidity test file for 'Ballot' contract & one JS test file for 'Storage' contract.
SCRIPTS
The 'scripts' folder has four typescript files which help to deploy the 'Storage' contract using 'web3.js' and 'ethers.js' libraries.
For the deployment of any other contract, just update the contract's name from 'Storage' to the desired contract and provide constructor arguments accordingly
in the file `deploy_with_ethers.ts` or `deploy_with_web3.ts`
In the 'tests' folder there is a script containing Mocha-Chai unit tests for 'Storage' contract.
To run a script, right click on file name in the file explorer and click 'Run'. Remember, Solidity file must already be compiled.
Output from script will appear in remix terminal.
Please note, require/import is supported in a limited manner for Remix supported modules.
For now, modules supported by Remix are ethers, web3, swarmgw, chai, multihashes, remix and hardhat only for hardhat.ethers object/plugin.
For unsupported modules, an error like this will be thrown: '<module_name> module require is not supported by Remix IDE' will be shown.
// this line is added to create a gist. Empty file is not allowed.
[package]
name = "Examples"
version = "0.0.0"
[addresses]
hello_blockchain = "0xcafe"
[dependencies]
AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-framework/", rev = "testnet" }
module hello_blockchain::message {
use std::error;
use std::signer;
use std::string;
use aptos_framework::account;
use aptos_framework::event;
//:!:>resource
struct MessageHolder has key {
message: string::String,
message_change_events: event::EventHandle<MessageChangeEvent>,
}
//<:!:resource
struct MessageChangeEvent has drop, store {
from_message: string::String,
to_message: string::String,
}
/// There is no message present
const ENO_MESSAGE: u64 = 0;
#[view]
public fun get_message(addr: address): string::String acquires MessageHolder {
assert!(exists<MessageHolder>(addr), error::not_found(ENO_MESSAGE));
*&borrow_global<MessageHolder>(addr).message
}
public entry fun set_message(account: signer, message: string::String)
acquires MessageHolder {
let account_addr = signer::address_of(&account);
if (!exists<MessageHolder>(account_addr)) {
move_to(&account, MessageHolder {
message,
message_change_events: account::new_event_handle<MessageChangeEvent>(&account),
})
} else {
let old_message_holder = borrow_global_mut<MessageHolder>(account_addr);
let from_message = *&old_message_holder.message;
event::emit_event(&mut old_message_holder.message_change_events, MessageChangeEvent {
from_message,
to_message: copy message,
});
old_message_holder.message = message;
}
}
#[test(account = @0x1)]
public entry fun sender_can_set_message(account: signer) acquires MessageHolder {
let addr = signer::address_of(&account);
aptos_framework::account::create_account_for_test(addr);
set_message(account, string::utf8(b"Hello, Blockchain"));
assert!(
get_message(addr) == string::utf8(b"Hello, Blockchain"),
ENO_MESSAGE
);
}
}
#[test_only]
module hello_blockchain::message_tests {
use std::signer;
use std::unit_test;
use std::vector;
use std::string;
use hello_blockchain::message;
fun get_account(): signer {
vector::pop_back(&mut unit_test::create_signers_for_testing(1))
}
#[test]
public entry fun sender_can_set_message() {
let account = get_account();
let addr = signer::address_of(&account);
aptos_framework::account::create_account_for_test(addr);
message::set_message(account, string::utf8(b"Hello, Blockchain"));
assert!(
message::get_message(addr) == string::utf8(b"Hello, Blockchain"),
0
);
}
}
[package]
name = "hello_prover"
version = "0.0.0"
[dependencies]
AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-framework/", rev = "main" }
[addresses]
module 0x42::prove {
fun plus1(x: u64): u64 {
// x+1
x+2 // error intended
}
spec plus1 {
ensures result == x+1;
}
fun abortsIf0(x: u64) {
if (x == 0) {
abort(0)
};
}
spec abortsIf0 {
aborts_if x == 0;
}
}
[package]
name = "MarketPlace"
version = "0.0.1"
[dependencies]
AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-framework/", rev = "devnet" }
AptosToken = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-token/", rev = "devnet" }
[addresses]
std = "0x1"
marketplace = "_"

Aptos NFT Marketplace Example

NOTE: THIS IS AN EXAMPLE AND HAS NOT BEEN FULLY AUDITED. THESE CONTRACTS ARE FOR COLLECTING FEEDBACK FROM OUR BUILDERS. ONCE WE ARE CONFIDENT THAT IT IS BENEFICIAL TO THE TOKEN ECOSYSTEM, WE WILL ADD IT TO the 0x3::aptos-token PACKAGE.

Introduction

The package contains two parts:

  • marketplace utility functions: these contracts specify the basic function and data structures for building a fixed-price marketplace and the auction house. The two contracts are: (1) marketplace_bid_utils and (2) marketplace_listing_utils
  • example marketplace contracts: these contracts show two examples of building a marketplace leveraging the marketplace utility functions.

Design principles

We want to have a minimal required example to improve the liquidity of the token ecosystem

  • Provide a unified Listing so that the same listing can be used across different marketplaces and aggregators.
  • Provide a unified buy and bid functions so that people can buy or bid for listed NFT across different marketplaces
  • Provide unified events so that downstream applications can have a clear overview of what is happening in the token ecosystem across all marketplaces

We want app developers to be creative with how they run their marketplace and auction house.

  • We separate the listing, buy and bid from other business logic to put them in utility functions.
  • We only provide example marketplace contracts for demonstration. Each marketplace is supposed to deploy its own contracts to its account. They decide how to charge the fee and how to select the bids that win the auction.

Design Choices

We also made the following design choices when we implemented the marketplace utility contracts. Any feedback on these choices is highly appreciated and helps the community.

  • Escrow-less listing: the seller can keep their tokens in their token stores and use the token (eg: show in the wallet, use the token, etc) before their token is sold.
  • The seller can choose who owns their listings. The listing can be stored under a marketplace account or stored under sellers' accounts. If the seller wants to work with a particular marketplace, they can give the listing to the marketplace to store after creating the listing. The marketplace can then decide how to expose the listing to the buyers. If the seller stores the listing under their own account, anyone can buy from these listings and these listings can be aggregated by aggregators.
  • Bidders have to lock their coins during the auction and can only withdraw the coin after the auction expires. Bidder can only increase their bid while the auction is still active. This ensures the bid is valid and the bidder cannot withdraw the coin while the auction is still active.

FAQ:

Why not store the token in the listing to guarantee the token exists?

We want to achieve two goals here, first, the token exists in the owner's token store before it is sold. second, the listed token should be available for transfer.

It is important to keep the token in the token store so that downstream apps, indexer, wallets can easily know the tokens owned by an account. The owner of the token can then use these listed tokens before the token is sold, as a listing can exist for a long time.

To check whether the listed token is available, there are many ways to handle this problem. For example, tracking the lister's token store events or using an indexer to verify if the owner still has the listed tokens. The marketplace can cancel the listing if the token balance is not enough.

Meanwhile, we will enhance the token store in our token standard to provide options to lock the token so that these tokens cannot be transferred out during the locking period.

How to support new features in this marketplace?

We will continuously collect new common features from the community and add them to the contracts in a backward-compatible way.

What is the plan for this package?

We plan to have these contracts in the move-example and collect the feedbacks from community. Once we have gone through enough iterations and be confident that it is beneficial to the token ecosystem, we will propose it to include them in the 0x3 aptos-token package.

/// This is an example demonstrating how to use marketplace_bid_utils and market_place_listing_utils to build an auction house
/// The basic flow can be found in test test_listing_one_and_two_bids
/// For more detailed description, check readme
module marketplace::marketplace_auction_example {
use aptos_framework::account;
use aptos_framework::coin;
use aptos_framework::event::{Self, EventHandle};
use aptos_framework::timestamp;
use aptos_std::simple_map::{Self, SimpleMap};
use aptos_std::table::{Self, Table};
use aptos_token::token::{Self, TokenId};
use marketplace::marketplace_bid_utils::{Self as bid, BidId, create_bid_id};
use marketplace::marketplace_listing_utils::{Self as listing, Listing, ListingEvent};
use std::error;
use std::signer;
use std::string::String;
use std::vector;
use aptos_framework::guid;
use aptos_token::property_map;
//
// Errors
//
/// Expiration time is invalid
const EINVALID_EXPIRATION_TIME: u64 = 1;
/// Start time is invalid
const EINVALID_START_TIME: u64 = 2;
/// Auction doesn't exist
const EAUCTION_NOT_EXIST: u64 = 3;
/// Bid increase less than minimal incremental
const EBID_INCREASE_TOO_SMALL: u64 = 4;
/// Auction ended
const EAUCTION_ENDED: u64 = 5;
/// Minimal incremental should be bigger than 0
const EBID_MIN_INCREMENTAL_IS_ZERO: u64 = 6;
/// Bid not found
const EBID_NOT_FOUND_FOR_AUCTION: u64 = 7;
/// Reserved operation for auction house owner
const EONLY_AUCTION_HOUSE_OWNER_CAN_PERFORM_THIS_OPERATION: u64 = 8;
/// Auction not ended
const EAUCTION_NOT_ENDED: u64 = 9;
/// Bid with same price exists for this auction
const EBID_WITH_SAME_PRICE_EXISTS: u64 = 10;
/// Bid not match the bid_id in the auction
const EBID_NOT_MATCH_ID_IN_AUCTION: u64 = 11;
/// Auction has zero bids
const EAUCION_HAS_ZERO_BIDS: u64 = 12;
/// Auction highest bid is zero
const EAUCTION_HIGHEST_BID_ZERO: u64 = 13;
struct AuctionHouseConfig has key {
market_fee_numerator: u64,
market_fee_denominator: u64,
fee_address: address,
}
struct Auctions<phantom CoinType> has key {
cur_auction_id: u64, // this is used to generate next auction_id
all_active_auctions: Table<u64, Auction<CoinType>>,
listing_event: EventHandle<ListingEvent>,
bid_event: EventHandle<BidEvent>,
cancel_bid_events: EventHandle<CancelBidEvent>
}
struct BidEvent has copy, drop, store {
market_address: address,
bid_id: BidId,
offer_price: u64,
expiration_sec: u64,
}
struct CancelBidEvent has copy, drop, store {
market_address: address,
bid_id: BidId,
}
struct Auction<phantom CoinType> has drop, store {
listing: Listing<CoinType>,
bids: SimpleMap<u64, BidId>, // mapping between the price and BidId
offer_numbers: vector<u64>, // the prices recorded for all the bids
}
public entry fun initialize_auction_house(
account: &signer,
market_fee_numerator: u64,
market_fee_denominator: u64,
fee_address: address,
) {
move_to(
account,
AuctionHouseConfig {
market_fee_denominator,
market_fee_numerator,
fee_address,
}
);
}
public entry fun initialize_auction<CoinType>(account: &signer) {
move_to(
account,
Auctions<CoinType> {
cur_auction_id: 0,
all_active_auctions: table::new(),
listing_event: account::new_event_handle<ListingEvent>(account),
bid_event: account::new_event_handle<BidEvent>(account),
cancel_bid_events: account::new_event_handle<CancelBidEvent>(account),
}
);
}
public fun generate_auction_data<CoinType>(
owner: &signer,
token_id: TokenId,
amount: u64,
min_price: u64,
start_sec: u64, // specify when the auction starts
expiration_sec: u64, // specify when the auction ends
withdraw_expiration_sec: u64,
): Auction<CoinType> {
let sec = timestamp::now_seconds();
assert!(sec <= start_sec, error::invalid_argument(EINVALID_START_TIME));
assert!(start_sec < expiration_sec, error::invalid_argument(EINVALID_EXPIRATION_TIME));
let listing = listing::create_listing<CoinType>(
owner,
token_id,
amount,
min_price,
false,
start_sec,
expiration_sec,
withdraw_expiration_sec,
vector<String>[],
vector<vector<u8>>[],
vector<String>[],
);
Auction<CoinType>{
listing,
bids: simple_map::create(),
offer_numbers: vector::empty(),
}
}
public entry fun create_auction<CoinType>(
owner: &signer,
creator: address,
collection_name: String,
token_name: String,
property_version: u64,
amount: u64,
min_price: u64,
start_sec: u64, // specify when the auction starts
expiration_sec: u64, // specify when the auction ends
withdraw_expiration_sec: u64, // specify deadline of token withdraw
) acquires Auctions {
let token_id = token::create_token_id_raw(creator, collection_name, token_name, property_version);
create_auction_with_token_id<CoinType>(
owner,
token_id,
amount,
min_price,
start_sec,
expiration_sec,
withdraw_expiration_sec,
);
}
public fun create_auction_with_token_id<CoinType>(
owner: &signer,
token_id: TokenId,
amount: u64,
min_price: u64,
start_sec: u64, // specify when the auction starts
listing_expiration_sec: u64, // specify when the auction ends
withdraw_expiration_sec: u64, // specify deadline of token withdraw
): u64 acquires Auctions {
let auction = generate_auction_data<CoinType>(
owner,
token_id,
amount,
min_price,
start_sec,
listing_expiration_sec,
withdraw_expiration_sec, // allow time to withdraw
);
// initialized coin store when listing
if (!coin::is_account_registered<CoinType>(signer::address_of(owner))) {
coin::register<CoinType>(owner);
};
let auctions = borrow_global_mut<Auctions<CoinType>>(@marketplace);
event::emit_event<ListingEvent>(
&mut auctions.listing_event,
listing::create_listing_event(
listing::get_listing_id(&auction.listing),
token_id,
amount,
min_price,
false,
start_sec,
listing_expiration_sec,
listing_expiration_sec + 50,
@marketplace,
property_map::empty(),
),
);
let next_id = auctions.cur_auction_id + 1;
*(&mut auctions.cur_auction_id) = next_id;
table::add(&mut auctions.all_active_auctions, next_id, auction);
next_id
}
public entry fun bid<CoinType>(
bidder: &signer,
creator: address,
collection_name: String,
token_name: String,
property_version: u64,
token_amount:u64,
offer_price: u64,
auction_id: u64,
withdraw_expiration_sec: u64,
) acquires Auctions {
// create bid and store it under the user account
let token_id = token::create_token_id_raw(creator, collection_name, token_name, property_version);
create_bid_with_token_id<CoinType>(bidder, token_id, token_amount, offer_price, auction_id, withdraw_expiration_sec);
}
/// Allow the bid to increase the coin for an existing bid
public entry fun increase_bid<CoinType>(
bidder: &signer,
price_delta: u64,
auction_id: u64,
) acquires Auctions {
let auctions = borrow_global_mut<Auctions<CoinType>>(@marketplace);
assert!(table::contains(&mut auctions.all_active_auctions, auction_id), error::not_found(EAUCTION_NOT_EXIST));
let auction = table::borrow_mut(&mut auctions.all_active_auctions, auction_id);
let listing_id = listing::get_listing_id<CoinType>(&auction.listing);
let bid_id = bid::create_bid_id(signer::address_of(bidder), listing_id);
increase_bid_price<CoinType>(
bidder,
bid_id,
price_delta,
auction_id,
)
}
/// Increase the offered price for an existing bid
/// The new price should not be same as any existing offered price
public fun increase_bid_price<CoinType>(
bidder: &signer,
bid_id: BidId,
price_delta: u64,
auction_id: u64,
) acquires Auctions {
let auctions = borrow_global_mut<Auctions<CoinType>>(@marketplace);
assert!(table::contains(&mut auctions.all_active_auctions, auction_id), error::not_found(EAUCTION_NOT_EXIST));
let auction = table::borrow_mut(&mut auctions.all_active_auctions, auction_id);
// get the listing info
// auction is still active
let now = timestamp::now_seconds();
assert!(now <= listing::get_listing_expiration<CoinType>(&auction.listing), error::invalid_argument(EAUCTION_ENDED));
// assert new offer_price is not duplicate price
let (old_price, _) = bid::get_bid_info<CoinType>(bid_id);
let new_offer_price = old_price + price_delta;
// check if same price exists previously, only bid with a different price can enter the auction
assert!(!simple_map::contains_key(&auction.bids, &new_offer_price), error::already_exists(EBID_WITH_SAME_PRICE_EXISTS));
bid::increase_bid(
bidder,
bid_id,
price_delta,
&auction.listing
);
}
public fun create_bid_with_token_id<CoinType>(
bidder: &signer,
token_id: TokenId,
token_amount:u64,
offer_price: u64,
auction_id: u64,
withdraw_expiration_sec: u64,
): BidId acquires Auctions {
let auctions = borrow_global_mut<Auctions<CoinType>>(@marketplace);
assert!(table::contains(&mut auctions.all_active_auctions, auction_id), error::not_found(EAUCTION_NOT_EXIST));
let auction = table::borrow_mut(&mut auctions.all_active_auctions, auction_id);
// initialize token store when bidding
token::initialize_token_store(bidder);
// get the listing info
// auction is still active
let now = timestamp::now_seconds();
assert!(now <= listing::get_listing_expiration<CoinType>(&auction.listing), error::invalid_argument(EAUCTION_ENDED));
// check if same price exists previously, only bid with a different price can enter the auction
assert!(!simple_map::contains_key(&auction.bids, &offer_price), error::already_exists(EBID_WITH_SAME_PRICE_EXISTS));
// allow participant to withdraw coin 60 secs after auction ends, configurable by each marketplace
let bid_id = bid::bid<CoinType>(
bidder,
token_id,
token_amount,
offer_price * token_amount,
&auction.listing,
withdraw_expiration_sec,
vector<String>[],
vector<vector<u8>>[],
vector<String>[],
);
event::emit_event<BidEvent>(
&mut auctions.bid_event,
BidEvent {
market_address: @marketplace,
bid_id,
offer_price,
expiration_sec: withdraw_expiration_sec,
},
);
// store the bid for this auction, only higher bid can enter the auction.
simple_map::add(&mut auction.bids, offer_price, bid_id);
vector::push_back(&mut auction.offer_numbers, offer_price);
bid_id
}
/// Auction house owner can remove auction from inventory
public fun remove_auction<CoinType>(account: &signer, auction_id: u64): Auction<CoinType> acquires Auctions {
assert!(signer::address_of(account) == @marketplace, error::permission_denied(EONLY_AUCTION_HOUSE_OWNER_CAN_PERFORM_THIS_OPERATION));
let auctions = borrow_global_mut<Auctions<CoinType>>(@marketplace);
table::remove(&mut auctions.all_active_auctions, auction_id)
}
/// Complete the auction, select the highest bid from existing bids and execute the bid against the listing
public entry fun complete_auction<CoinType>(account: &signer, auction_id: u64) acquires Auctions, AuctionHouseConfig {
assert!(signer::address_of(account) == @marketplace, error::permission_denied(EONLY_AUCTION_HOUSE_OWNER_CAN_PERFORM_THIS_OPERATION));
let auctions = borrow_global_mut<Auctions<CoinType>>(@marketplace);
assert!(table::contains(&auctions.all_active_auctions, auction_id), error::not_found(EAUCTION_NOT_EXIST));
let auction = table::borrow_mut(&mut auctions.all_active_auctions, auction_id);
let expiration_time = listing::get_listing_expiration<CoinType>(&auction.listing);
let now = timestamp::now_seconds();
assert!(now >= expiration_time, error::invalid_state(EAUCTION_NOT_ENDED));
let config = borrow_global<AuctionHouseConfig>(@marketplace);
let auction = remove_auction<CoinType>(account, auction_id);
let highest_bid_id = find_highest_bid(&auction);
let Auction {
listing,
bids,
offer_numbers: _,
} = auction;
if ( simple_map::length(&bids) > 0) {
// get the bid corresponding to highest price
bid::execute_listing_bid<CoinType>(
highest_bid_id,
listing,
config.fee_address,
config.market_fee_numerator,
config.market_fee_denominator,
);
};
}
/// The same function exists in the marketplace bid utils.
/// Have this function here is to make the marketplace feature complete since the marketplace contract should also
/// allow users an entry function to withdraw coin
public entry fun withdraw_coin_from_bid<CoinType>(
bidder: &signer,
lister_addr: address,
listing_creation_number: u64,
) {
bid::withdraw_coin_from_bid<CoinType>(bidder, lister_addr, listing_creation_number);
}
/// bidder can remove their bid from the auction so that the bid won't participate in auction
/// This doesn't withdraw the actual coin from the bid
public entry fun cancel_bid_in_auction<CoinType>(
bidder: &signer,
auction_id: u64,
) acquires Auctions {
let auctions = borrow_global_mut<Auctions<CoinType>>(@marketplace);
assert!(table::contains(&auctions.all_active_auctions, auction_id), error::not_found(EAUCTION_NOT_EXIST));
let auction = table::borrow_mut(&mut auctions.all_active_auctions, auction_id);
let now = timestamp::now_seconds();
assert!(now < listing::get_listing_expiration<CoinType>(&auction.listing), error::invalid_argument(EAUCTION_ENDED));
let listing_id = listing::get_listing_id<CoinType>(&auction.listing);
let bidder_address = signer::address_of(bidder);
let bid_id = create_bid_id(bidder_address, listing_id);
let (offer_price, _) = bid::get_bid_info<CoinType>(bid_id);
assert!(simple_map::contains_key(&mut auction.bids, &offer_price), error::not_found(EBID_NOT_FOUND_FOR_AUCTION));
assert!(
*simple_map::borrow(&mut auction.bids, &offer_price) == bid_id,
error::permission_denied(EBID_NOT_MATCH_ID_IN_AUCTION)
);
simple_map::remove(&mut auction.bids, &offer_price);
let (found, index) = vector::index_of(&mut auction.offer_numbers, &offer_price);
assert!(found, error::not_found(EBID_NOT_FOUND_FOR_AUCTION));
vector::swap_remove(&mut auction.offer_numbers, index);
event::emit_event<CancelBidEvent>(
&mut auctions.cancel_bid_events,
CancelBidEvent {
market_address: @marketplace,
bid_id,
},
);
}
/// Get the listing id corresponding to a auction
public fun get_auction_listing_id<CoinType>(auction_id: u64): guid::ID acquires Auctions {
let auctions = borrow_global_mut<Auctions<CoinType>>(@marketplace);
assert!(table::contains(&auctions.all_active_auctions, auction_id), error::not_found(EAUCTION_NOT_EXIST));
let auction = table::borrow_mut(&mut auctions.all_active_auctions, auction_id);
listing::get_listing_id<CoinType>(&auction.listing)
}
fun find_highest_bid<CoinType>(auction: &Auction<CoinType>): BidId {
assert!(simple_map::length(&auction.bids) > 0, error::invalid_state(EAUCION_HAS_ZERO_BIDS));
let highest_price = 0;
let ind = 0;
while (ind < vector::length(&auction.offer_numbers)) {
let price = *vector::borrow(&auction.offer_numbers, ind);
if (price > highest_price) {
highest_price = price;
};
ind = ind + 1;
};
assert!(highest_price > 0, error::invalid_state(EAUCTION_HIGHEST_BID_ZERO));
*simple_map::borrow(&auction.bids, &highest_price)
}
#[test(lister = @marketplace, bidder_a = @0xBB, bidder_b = @0xBA, framework = @0x1, house = @marketplace, fee_account = @0xa)]
public fun test_listing_one_and_two_bids(
lister: signer,
bidder_a: signer,
bidder_b: signer,
framework: signer,
house: signer,
fee_account: signer,
) acquires Auctions, AuctionHouseConfig {
use aptos_framework::coin;
use aptos_framework::account;
timestamp::set_time_has_started_for_testing(&framework);
timestamp::update_global_time_for_test(1);
account::create_account_for_test(signer::address_of(&lister));
account::create_account_for_test(signer::address_of(&bidder_a));
account::create_account_for_test(signer::address_of(&bidder_b));
account::create_account_for_test(signer::address_of(&framework));
account::create_account_for_test(signer::address_of(&fee_account));
// setup the auction house global fee config and config for each coin type
initialize_auction_house(
&house,
1,
100,
signer::address_of(&fee_account)
);
initialize_auction<coin::FakeMoney>(&house);
// owner creats a listing
let token_id = token::create_collection_and_token(
&lister,
2,
2,
2,
vector<String>[],
vector<vector<u8>>[],
vector<String>[],
vector<bool>[false, false, false],
vector<bool>[false, false, false, false, true],
);
let (creator, collection, name, version) = token::get_token_id_fields(&token_id);
create_auction<coin::FakeMoney>(
&lister,
creator,
collection,
name,
version,
1,
1,
1,
2,
2 + 10,
);
timestamp::update_global_time_for_test(1000000);
coin::create_fake_money(&framework, &bidder_a, 1000);
coin::register<coin::FakeMoney>(&bidder_b);
coin::register<coin::FakeMoney>(&fee_account);
coin::transfer<coin::FakeMoney>(&framework, signer::address_of(&bidder_a), 500);
coin::transfer<coin::FakeMoney>(&framework, signer::address_of(&bidder_b), 500);
bid<coin::FakeMoney>(
&bidder_a,
creator,
collection,
name,
version,
1,
100,
1,
1 + 10,
);
bid<coin::FakeMoney>(
&bidder_b,
creator,
collection,
name,
version,
1,
300,
1,
1 + 10,
);
timestamp::update_global_time_for_test(3000000);
complete_auction<coin::FakeMoney>(&house, 1);
// highest bidder bidder B get the token
assert!(token::balance_of(signer::address_of(&bidder_b), token_id) == 1, 1);
assert!(token::balance_of(signer::address_of(&bidder_a), token_id) == 0, 1);
// 3 coin is paid for market fee and remaining is 297
assert!(coin::balance<coin::FakeMoney>(signer::address_of(&lister)) == 297, 1);
}
#[test(lister = @marketplace, bidder_a = @0xBB, framework = @0x1, house = @marketplace)]
public fun test_cancel_bid(
lister: signer,
bidder_a: signer,
framework: signer,
house: signer,
) acquires Auctions {
use aptos_framework::coin;
use aptos_framework::account;
timestamp::set_time_has_started_for_testing(&framework);
timestamp::update_global_time_for_test(1);
account::create_account_for_test(signer::address_of(&lister));
account::create_account_for_test(signer::address_of(&bidder_a));
account::create_account_for_test(signer::address_of(&framework));
// setup the auction house global fee config and config for each coin type
initialize_auction_house(
&house,
1,
100,
signer::address_of(&house)
);
initialize_auction<coin::FakeMoney>(&house);
// owner creats a listing
let token_id = token::create_collection_and_token(
&lister,
2,
2,
2,
vector<String>[],
vector<vector<u8>>[],
vector<String>[],
vector<bool>[false, false, false],
vector<bool>[false, false, false, false, true],
);
let auction_id = create_auction_with_token_id<coin::FakeMoney>(
&lister,
token_id,
1,
1,
12,
20,
20 + 50,
);
coin::create_fake_money(&framework, &bidder_a, 1000);
coin::transfer<coin::FakeMoney>(&framework, signer::address_of(&bidder_a), 500);
timestamp::update_global_time_for_test(12000000);
create_bid_with_token_id<coin::FakeMoney>(
&bidder_a,
token_id,
1,
100,
auction_id,
20 + 50,
);
// bid_id_creation_number should be shown to users to allow them cancel the bid
cancel_bid_in_auction<coin::FakeMoney>(&bidder_a, auction_id);
let auction = table::borrow(
&borrow_global<Auctions<coin::FakeMoney>>(signer::address_of(&house)).all_active_auctions,
auction_id
);
assert!(simple_map::length(&auction.bids) == 0, 1);
timestamp::update_global_time_for_test(300000000);
let listing_id = get_auction_listing_id<coin::FakeMoney>(auction_id);
withdraw_coin_from_bid<coin::FakeMoney>(
&bidder_a,
guid::id_creator_address(&listing_id),
guid::id_creation_num(&listing_id)
);
}
}
/// An marketplace library providing basic function for buy and bid
/// To see how to use the library, please check the two example contract in the same folder
module marketplace::marketplace_bid_utils {
use aptos_framework::account;
use aptos_framework::coin::{Self, Coin};
use aptos_framework::event::{Self, EventHandle};
use aptos_framework::timestamp;
use aptos_std::guid::{Self, ID};
use aptos_std::table::{Self, Table};
use aptos_token::token::{Self, TokenId};
use marketplace::marketplace_listing_utils::{Self as listing_util, Listing, create_listing_id_raw};
use std::signer;
use std::error;
use std::string::String;
use aptos_token::property_map::{Self, PropertyMap};
//
// Errors
//
/// No sufficient fund to bid
const ENO_SUFFICIENT_FUND: u64 = 1;
/// Token ID doesn't match
const ETOKEN_ID_NOT_MATCH: u64 = 2;
/// Listing expired
const ELISTING_EXPIRED: u64 = 3;
/// Listing hasn't started yet
const ELISTING_NOT_STARTED: u64 = 4;
/// Token amount doesn't match
const ETOKEN_AMOUNT_NOT_MATCH: u64 = 5;
/// Bid doesn't exist
const EBID_NOT_EXIST: u64 = 6;
/// Cannot withdraw fund before bid expiration time
const ECANNOT_DRAW_FUND_BEFORE_EXPIRATION_TIME: u64 = 7;
/// Listing Id doesn't match
const ELISTING_ID_NOT_MATCH: u64 = 8;
/// The bidder has already bid for the same listing
const EBID_ID_EXISTS: u64 = 9;
/// Buy from non-instant sale listing
const EBUY_NON_INSTANT_SALE_LISTING: u64 = 10;
/// Cannot buy from expired listing
const EBUY_FROM_EXPIRED_LISTING: u64 = 11;
/// Cannot buy from a listing that hasn't started
const EBUY_FROM_NOT_STARTED_LISTING: u64 = 12;
/// hold the bid info and coin at user account
struct Bid<phantom CoinType> has store {
id: BidId,
coin: Coin<CoinType>,
offer_price: u64,
expiration_sec: u64,
config: PropertyMap,
}
/// This is the BidId used dedup the bid from the same signer for a listing
struct BidId has copy, drop, store {
bidder: address,
listing_id: ID,
}
/// store all the bids by the user
struct BidRecords<phantom CoinType> has key {
records: Table<BidId, Bid<CoinType>>,
bid_event: EventHandle<BidEvent<CoinType>>,
withdraw_bid_event: EventHandle<WithdrawBidEvent<CoinType>>,
order_executed_event: EventHandle<OrderExecutedEvent<CoinType>>,
increase_bid_event: EventHandle<IncreaseBidEvent<CoinType>>,
}
struct BidEvent<phantom CoinType> has copy, drop, store {
offer_price: u64,
bid_id: BidId,
expiration_sec: u64,
}
struct IncreaseBidEvent<phantom CoinType> has copy, drop, store {
new_price: u64,
bid_id: BidId,
}
struct WithdrawBidEvent<phantom CoinType> has copy, drop, store {
bid_id: BidId,
}
struct OrderExecutedEvent<phantom CoinType> has copy, drop, store {
buyer: address,
lister_address: address,
listing_creation_number: u64,
executed_price: u64,
market_place_address: address,
}
//
// entry functions
//
/// Allow buyer to directly buy from a listing directly listed under an account without paying any fee
public entry fun buy_from_owner_with_fee<CoinType>(
buyer: &signer,
lister_address: address,
listing_creation_number: u64,
market_fee_address: address,
fee_numerator: u64,
fee_denominator: u64,
) acquires BidRecords {
let entry = listing_util::remove_listing<CoinType>(lister_address, listing_creation_number);
buy_from_listing_with_fee<CoinType>(buyer, entry, market_fee_address, fee_numerator, fee_denominator);
}
/// Bidder can withdraw the bid after the bid expires to get the coin back and store them in the coinstore
public entry fun withdraw_coin_from_bid<CoinType>(
bidder: &signer,
lister_addr: address,
listing_creation_number: u64,
) acquires BidRecords {
let bidder_address = signer::address_of(bidder);
let listing_id = create_listing_id_raw(lister_addr, listing_creation_number);
let bid_id = create_bid_id(bidder_address, listing_id);
let bid_records = borrow_global_mut<BidRecords<CoinType>>(bidder_address);
assert!(table::contains(&bid_records.records, bid_id), error::not_found(EBID_NOT_EXIST));
let bid = table::remove(&mut bid_records.records, bid_id);
assert!(timestamp::now_seconds() > bid.expiration_sec, error::permission_denied(ECANNOT_DRAW_FUND_BEFORE_EXPIRATION_TIME));
coin::deposit(bidder_address, clear_bid(bid));
event::emit_event<WithdrawBidEvent<CoinType>>(
&mut bid_records.withdraw_bid_event,
WithdrawBidEvent<CoinType> {
bid_id
},
);
}
//
// public functions
//
/// Buy from listings. This can be called by marketplace contracts with their own fee config and stored Listing
public fun buy_from_listing_with_fee<CoinType>(
buyer: &signer,
entry: Listing<CoinType>,
market_fund_address: address,
fee_numerator: u64,
fee_denominator: u64,
) acquires BidRecords {
// assert the listing is active
let (
id,
token_id,
listed_amount,
min_price,
instant_sale,
start_sec,
expiration_sec,
withdraw_cap,
_,
) = listing_util::destroy_listing(entry);
let now = timestamp::now_seconds();
assert!(now > start_sec, error::invalid_argument(EBUY_FROM_NOT_STARTED_LISTING));
assert!(now < expiration_sec, error::invalid_argument(EBUY_FROM_EXPIRED_LISTING));
// listing is instant sale
assert!(instant_sale, error::invalid_argument(EBUY_NON_INSTANT_SALE_LISTING));
// assert the buyer has sufficient balance
let buyer_addr = signer::address_of(buyer);
let required_balance = min_price * listed_amount;
// check bidder has sufficient balance
assert!(coin::balance<CoinType>(buyer_addr) >= required_balance, error::invalid_argument(ENO_SUFFICIENT_FUND));
initialize_bid_records<CoinType>(buyer);
// swap the coin and token
let token = token::withdraw_with_capability(
withdraw_cap
);
token::deposit_token(buyer, token);
let coins = coin::withdraw<CoinType>(buyer, required_balance);
// deduct royalty fee from the transactions
let royalty = token::get_royalty(token_id);
let royalty_payee = token::get_royalty_payee(&royalty);
let royalty_coin = deduct_fee<CoinType>(
&mut coins,
token::get_royalty_numerator(&royalty),
token::get_royalty_denominator(&royalty)
);
coin::deposit(royalty_payee, royalty_coin);
// deduct marketplace fee
let market_fee = deduct_fee<CoinType>(&mut coins, fee_numerator, fee_denominator);
coin::deposit(market_fund_address, market_fee);
// give the remaining to the seller
let token_owner = guid::id_creator_address(&id);
coin::deposit(token_owner, coins);
emit_order_executed_event<CoinType>(
buyer_addr,
token_owner,
guid::id_creation_num(&id),
min_price,
market_fund_address,
);
}
public fun initialize_bid_records<CoinType>(bidder: &signer) {
let owner_addr = signer::address_of(bidder);
if (!exists<BidRecords<CoinType>>(owner_addr)) {
move_to(
bidder,
BidRecords<CoinType> {
records: table::new(),
bid_event: account::new_event_handle<BidEvent<CoinType>>(bidder),
withdraw_bid_event: account::new_event_handle<WithdrawBidEvent<CoinType>>(bidder),
increase_bid_event: account::new_event_handle<IncreaseBidEvent<CoinType>>(bidder),
order_executed_event: account::new_event_handle<OrderExecutedEvent<CoinType>>(bidder),
}
);
};
}
/// withdraw the coin and store them in bid struct and return a global unique bid id
public fun bid<CoinType>(
bidder: &signer,
token_id: TokenId,
token_amount:u64,
offer_price: u64,
entry: &Listing<CoinType>,
expiration_sec: u64,
keys: vector<String>,
values: vector<vector<u8>>,
types: vector<String>,
): BidId acquires BidRecords {
initialize_bid_records<CoinType>(bidder);
let bidder_address = signer::address_of(bidder);
// check the bid is legit for the listing
let total_coin_amount = offer_price * token_amount; // the total coin offerred by the bidder
// check bidder has sufficient balance
assert!(coin::balance<CoinType>(bidder_address) >= total_coin_amount, error::invalid_argument(ENO_SUFFICIENT_FUND));
assert_bid_parameters(token_id, total_coin_amount, token_amount, entry, timestamp::now_seconds());
// assert the bid_id not exist in the bid records
initialize_bid_records<CoinType>(bidder);
let bid_records = borrow_global_mut<BidRecords<CoinType>>(bidder_address);
let bid_id = create_bid_id(bidder_address, listing_util::get_listing_id(entry));
assert!(!table::contains(&bid_records.records, bid_id), error::already_exists(EBID_ID_EXISTS));
// withdraw the coin and store them in escrow to ensure the fund is avaliable until expiration_sec
let coin = coin::withdraw<CoinType>(bidder, total_coin_amount);
let bid = Bid<CoinType> {
id: bid_id,
coin,
offer_price,
expiration_sec,
config: property_map::new(keys, values, types),
};
table::add(&mut bid_records.records, bid_id, bid);
event::emit_event<BidEvent<CoinType>>(
&mut bid_records.bid_event,
BidEvent<CoinType> {
offer_price,
bid_id,
expiration_sec,
},
);
// opt-in direct transfer to receive token without signer
token::opt_in_direct_transfer(bidder, true);
bid_id
}
/// Allow the bid to increase the coin for an existing bid
public fun increase_bid<CoinType>(
bidder: &signer,
bid_id: BidId,
price_delta: u64,
entry: &Listing<CoinType>,
) acquires BidRecords {
let bidder_address = signer::address_of(bidder);
let bid_records = borrow_global_mut<BidRecords<CoinType>>(bidder_address);
assert!(table::contains(&bid_records.records, bid_id), error::not_found(EBID_NOT_EXIST));
let listing_id = listing_util::get_listing_id(entry);
assert!(bid_id.listing_id == listing_id, error::invalid_argument(ELISTING_ID_NOT_MATCH));
// check the bid is legit for the listing
let token_amount = listing_util::get_listing_token_amount(entry);
let added_amount = price_delta * token_amount;
// check bidder has sufficient balance
assert!(coin::balance<CoinType>(bidder_address) >= added_amount, error::invalid_argument(ENO_SUFFICIENT_FUND));
// add coin to the bid and update its info
let added_coin = coin::withdraw<CoinType>(bidder, added_amount);
let bid = table::borrow_mut(&mut bid_records.records, bid_id);
bid.offer_price = bid.offer_price + price_delta;
coin::merge(&mut bid.coin, added_coin);
event::emit_event<IncreaseBidEvent<CoinType>>(
&mut bid_records.increase_bid_event,
IncreaseBidEvent<CoinType> {
new_price: bid.offer_price,
bid_id,
},
);
}
/// execute a bid to a listing, no signer required to perform this function
/// pay fee to 3rd party based on a percentage
/// deduct royalty and send to the payee account
/// only the listing owner can execute the bid
public fun execute_listing_bid<CoinType>(
bid_id: BidId,
entry: Listing<CoinType>,
market_fund_address: address,
fee_numerator: u64,
fee_denominator: u64,
) acquires BidRecords {
let bid_records = &mut borrow_global_mut<BidRecords<CoinType>>(bid_id.bidder).records;
assert!(table::contains(bid_records, bid_id), error::not_found(EBID_NOT_EXIST));
let bid = table::borrow(bid_records, bid_id);
let (
id,
token_id,
listed_amount,
min_price,
_,
_,
expiration_sec,
withdraw_cap,
_,
) = listing_util::destroy_listing(entry);
let coin_owner = bid.id.bidder;
// validate offerred amount and price
let min_total = min_price * listed_amount;
assert!(coin::value(&bid.coin) >= min_total, error::invalid_argument(ENO_SUFFICIENT_FUND));
// validate expiration time
let now = timestamp::now_seconds();
assert!(now >= expiration_sec, error::invalid_argument(ELISTING_EXPIRED));
//listing_id matches
assert!(id == bid.id.listing_id, error::invalid_argument(ELISTING_ID_NOT_MATCH));
// transfer coin and token
let token = token::withdraw_with_capability(
withdraw_cap
);
token::direct_deposit_with_opt_in(coin_owner, token);
let bid_mut = table::remove(bid_records, bid_id);
let offer_price = bid_mut.offer_price;
let coins = clear_bid(bid_mut);
// deduct royalty fee from the transactions
let royalty = token::get_royalty(token_id);
let royalty_payee = token::get_royalty_payee(&royalty);
let royalty_coin = deduct_fee<CoinType>(
&mut coins,
token::get_royalty_numerator(&royalty),
token::get_royalty_denominator(&royalty)
);
coin::deposit(royalty_payee, royalty_coin);
// deduct marketplace fee
let market_fee = deduct_fee<CoinType>(&mut coins, fee_numerator, fee_denominator);
coin::deposit(market_fund_address, market_fee);
// give the remaining to the seller
let token_owner = guid::id_creator_address(&id);
coin::deposit(token_owner, coins);
emit_order_executed_event<CoinType>(
coin_owner,
token_owner,
guid::id_creation_num(&id),
offer_price,
market_fund_address,
);
}
/// validate if bid is legit for a listing.
public fun assert_bid_parameters<CoinType>(
token_id: TokenId,
offer_price: u64,
token_amount: u64,
entry: &Listing<CoinType>,
bid_time: u64,
) {
// validate token_id match
assert!(token_id == listing_util::get_listing_token_id(entry), error::invalid_argument(ETOKEN_ID_NOT_MATCH));
// validate offerred amount and price
let listed_amount = listing_util::get_listing_token_amount(entry);
let min_total = listing_util::get_listing_min_price(entry) * listed_amount;
let total_coin_amount = offer_price * token_amount;
assert!(total_coin_amount >= min_total, ENO_SUFFICIENT_FUND);
assert!(token_amount == listed_amount, ETOKEN_AMOUNT_NOT_MATCH);
assert!(bid_time >= listing_util::get_listing_start(entry), error::invalid_argument(ELISTING_NOT_STARTED));
assert!(bid_time <= listing_util::get_listing_expiration(entry), error::invalid_argument(ELISTING_EXPIRED));
}
public fun get_bid_info<CoinType>(
bid_id: BidId
): (u64, u64) acquires BidRecords {
let bid_records = &mut borrow_global_mut<BidRecords<CoinType>>(bid_id.bidder).records;
assert!(table::contains(bid_records, bid_id), error::not_found(EBID_NOT_EXIST));
let bid = table::borrow(bid_records, bid_id);
(bid.offer_price, bid.expiration_sec)
}
/// internal function for assigned a global unique id for a listing
public fun create_bid_id(bidder: address, listing_id: ID): BidId {
BidId {
bidder,
listing_id,
}
}
/// get bidder address from BidId
public fun get_bid_id_address(bid_id: &BidId): address {
bid_id.bidder
}
/// get bidder listing id from BidId
public fun get_bid_id_listing_id(bid_id: &BidId): ID {
bid_id.listing_id
}
//
// Private or friend functions
//
/// destruct the bid struct and extract coins
fun clear_bid<CoinType>(bid: Bid<CoinType>): Coin<CoinType> {
let Bid {
id: _,
coin,
offer_price: _,
expiration_sec: _,
config: _
} = bid;
coin
}
fun emit_order_executed_event<CoinType>(
buyer: address,
lister_address: address,
listing_creation_number: u64,
executed_price: u64,
market_place_address: address,
) acquires BidRecords {
let records = borrow_global_mut<BidRecords<CoinType>>(buyer);
event::emit_event<OrderExecutedEvent<CoinType>>(
&mut records.order_executed_event,
OrderExecutedEvent<CoinType> {
buyer,
lister_address,
listing_creation_number,
executed_price,
market_place_address,
},
);
}
fun deduct_fee<CoinType>(
total_coin: &mut Coin<CoinType>,
fee_numerator: u64,
fee_denominator: u64
): Coin<CoinType> {
let value = coin::value(total_coin);
let fee = if (fee_denominator == 0) {
0
} else {
value * fee_numerator/ fee_denominator
};
coin::extract(total_coin, fee)
}
#[test_only]
public fun test_aution_setup(
owner: &signer,
bidder_a: &signer,
aptos_framework: &signer,
use_wrong_coin_amount: bool,
use_wrong_token_amount: bool,
): (BidId, Listing<coin::FakeMoney>) acquires BidRecords {
timestamp::set_time_has_started_for_testing(aptos_framework);
timestamp::update_global_time_for_test(11000000);
account::create_account_for_test(signer::address_of(owner));
account::create_account_for_test(signer::address_of(bidder_a));
account::create_account_for_test(signer::address_of(aptos_framework));
// owner creats a listing
let token_id = token:: create_collection_and_token(
owner,
2,
2,
2,
vector<String>[],
vector<vector<u8>>[],
vector<String>[],
vector<bool>[false, false, false],
vector<bool>[false, false, false, false, false],
);
let entry = listing_util::create_listing<coin::FakeMoney>(
owner,
token_id,
1,
2,
false,
0,
100,
200,
vector<String>[],
vector<vector<u8>>[],
vector<String>[],
);
coin::create_fake_money(aptos_framework, bidder_a, 100);
coin::transfer<coin::FakeMoney>(aptos_framework, signer::address_of(bidder_a), 100);
//assert!(signer::address_of(&owner) == @0x1, 1);
token::initialize_token_store(bidder_a);
coin::register<coin::FakeMoney>(owner);
let token_amount = if (use_wrong_token_amount) { 10 } else {1};
let offered_price = if (use_wrong_coin_amount) {1} else {10};
let bid_1 = bid<coin::FakeMoney>(
bidder_a,
token_id,
token_amount,
offered_price,
&entry,
100000001,
vector<String>[],
vector<vector<u8>>[],
vector<String>[],
);
(bid_1, entry)
}
#[test(owner = @0xFE, bidder_a = @0xBC, aptos_framework = @aptos_framework)]
public fun test_successful(
owner: signer,
bidder_a: signer,
aptos_framework: signer
) acquires BidRecords {
let (bid_id, entry) = test_aution_setup(
&owner,
&bidder_a,
&aptos_framework,
false,
false,
);
let lister = listing_util::get_listing_creator(&entry);
timestamp::update_global_time_for_test(100000000);
execute_listing_bid(bid_id, entry,@aptos_framework, 10, 100);
// listing owner get paid with a deduction of market fee
// 1 * 10 - (1 * 10) * (10 / 100)
assert!(coin::balance<coin::FakeMoney>(lister) == 9, 1);
}
#[test(owner = @marketplace, bidder_a = @0xBB, aptos_framework = @aptos_framework)]
#[expected_failure(abort_code = 1, location = marketplace::marketplace_bid_utils)]
public fun test_wrong_coin_amount(
owner: signer,
bidder_a: signer,
aptos_framework: signer
) acquires BidRecords {
let (bid_id, entry) = test_aution_setup(
&owner,
&bidder_a,
&aptos_framework,
true,
false,
);
timestamp::update_global_time_for_test(100000000);
execute_listing_bid(bid_id, entry, @aptos_framework, 0, 1);
}
#[test(owner = @marketplace, bidder_a = @0xBB, aptos_framework = @aptos_framework)]
#[expected_failure(abort_code = 5, location = marketplace::marketplace_bid_utils)]
public fun test_wrong_token_amount(
owner: signer,
bidder_a: signer,
aptos_framework: signer
) acquires BidRecords {
let (bid_id, entry) = test_aution_setup(
&owner,
&bidder_a,
&aptos_framework,
false,
true,
);
timestamp::update_global_time_for_test(100000000);
execute_listing_bid(bid_id, entry, @aptos_framework, 0, 1);
}
#[test(owner = @marketplace, bidder_a = @0xBB, aptos_framework = @aptos_framework)]
public fun test_increase_bid(
owner: signer,
bidder_a: signer,
aptos_framework: signer
) acquires BidRecords {
let (bid_id, entry) = test_aution_setup(
&owner,
&bidder_a,
&aptos_framework,
false,
false,
);
increase_bid(&bidder_a, bid_id, 10, &entry);
assert!(coin::balance<coin::FakeMoney>(signer::address_of(&bidder_a)) == 80, 1);
}
#[test_only]
public fun test_instant_sale_setup(
owner: &signer,
buyer: &signer,
aptos_framework: &signer,
start_sec: u64,
end_sec: u64,
): (Listing<coin::FakeMoney>, TokenId) {
timestamp::set_time_has_started_for_testing(aptos_framework);
account::create_account_for_test(signer::address_of(owner));
account::create_account_for_test(signer::address_of(buyer));
account::create_account_for_test(signer::address_of(aptos_framework));
// owner creats a listing
let token_id = token::create_collection_and_token(
owner,
2,
2,
2,
vector<String>[],
vector<vector<u8>>[],
vector<String>[],
vector<bool>[false, false, false],
vector<bool>[false, false, false, false, false],
);
let entry = listing_util::create_listing<coin::FakeMoney>(
owner,
token_id,
1,
100,
true,
start_sec,
end_sec,
end_sec + 1, // token transfer happens immedidately after buying
vector<String>[],
vector<vector<u8>>[],
vector<String>[],
);
coin::create_fake_money(aptos_framework, buyer, 100);
coin::transfer<coin::FakeMoney>(aptos_framework, signer::address_of(buyer), 100);
//assert!(signer::address_of(&owner) == @0x1, 1);
token::initialize_token_store(buyer);
coin::register<coin::FakeMoney>(owner);
(entry, token_id)
}
#[test(owner = @marketplace, buyer = @0xBB, framework = @aptos_framework, market = @0x33)]
fun test_buy_successful(
owner: &signer,
buyer: &signer,
framework: &signer,
market: &signer,
) acquires BidRecords {
account::create_account_for_test(signer::address_of(market));
coin::register<coin::FakeMoney>(market);
let (entry, token_id) = test_instant_sale_setup(owner, buyer, framework, 1, 10);
timestamp::update_global_time_for_test(2000000);
let owner_addr = signer::address_of(owner);
let buyer_addr = signer::address_of(buyer);
buy_from_listing_with_fee(
buyer,
entry,
signer::address_of(market),
1,
100,
);
// assert the token and coin are transferred as expected
assert!(token::balance_of(owner_addr, token_id) == 1, 1);
assert!(token::balance_of(buyer_addr, token_id) == 1, 1);
assert!(coin::balance<coin::FakeMoney>(buyer_addr) == 0, 1);
// 1 % is paid as market fee
assert!(coin::balance<coin::FakeMoney>(owner_addr) == 99, 1);
}
#[test(owner = @0x12, buyer = @0x34, framework = @aptos_framework)]
#[expected_failure(abort_code = 65538, location = aptos_framework::timestamp)]
fun test_buy_before_start(
owner: &signer,
buyer: &signer,
framework: &signer,
) acquires BidRecords {
let (entry, _) = test_instant_sale_setup(owner, buyer, framework, 1, 10);
timestamp::update_global_time_for_test(0);
buy_from_listing_with_fee(
buyer,
entry,
signer::address_of(framework),
1,
100,
);
}
#[test(owner = @0x12, buyer = @0x34, framework = @aptos_framework)]
#[expected_failure(abort_code = 65547, location = marketplace::marketplace_bid_utils)]
fun test_buy_after_expire(
owner: &signer,
buyer: &signer,
framework: &signer,
) acquires BidRecords {
let (entry, _) = test_instant_sale_setup(owner, buyer, framework, 1, 10);
timestamp::update_global_time_for_test(30000000);
buy_from_listing_with_fee(
buyer,
entry,
signer::address_of(framework),
1,
100,
);
}
#[test(owner = @marketplace, bidder_a = @0xBB, framework = @aptos_framework, buyer = @0xee)]
#[expected_failure(abort_code = 65546, location = marketplace::marketplace_bid_utils)]
fun test_buy_from_auction_listing(
owner: &signer,
bidder_a: &signer,
framework: &signer,
buyer: &signer,
) acquires BidRecords {
let (_, entry) = test_aution_setup(
owner,
bidder_a,
framework,
false,
false,
);
buy_from_listing_with_fee(
buyer,
entry,
signer::address_of(framework),
1,
100,
);
}
}
/// This is an example demonstrating how to use marketplace_bid_utils and market_place_listing_utils to build an auction house
/// This example shows how to build a decentralized marketplace where listing are stored under owner's account
/// Note: the buyer can buy from any listing that is stored under owners' account
/// For more detailed description, check readme
module marketplace::marketplace_instant_sale_example {
use std::string::String;
use aptos_std::table::Table;
use marketplace::marketplace_listing_utils::{Self as listing_utils, Listing};
use marketplace::marketplace_bid_utils::{Self as bid_utils};
use aptos_framework::guid::ID;
struct Config has key {
market_fee_numerator: u64,
market_fee_denominator: u64,
fee_address: address,
}
public entry fun initialize_market(
account: &signer,
market_fee_numerator: u64,
market_fee_denominator: u64,
fee_address: address,
) {
move_to(
account,
Config {
market_fee_denominator,
market_fee_numerator,
fee_address,
}
);
}
struct Listings<phantom CoinType> has key {
all_active_Listings: Table<ID, Listing<CoinType>>,
}
public entry fun creat_listing<CoinType>(
owner: &signer,
creator: address,
collection_name: String,
token_name: String,
property_version: u64,
amount: u64,
min_price: u64,
start_sec: u64,
expiration_sec: u64,
withdraw_expiration_sec: u64,
) {
listing_utils::direct_listing<CoinType>(
owner,
creator,
collection_name,
token_name,
property_version,
amount,
min_price,
true,
start_sec,
expiration_sec,
withdraw_expiration_sec,
);
}
public entry fun buy_listing<CoinType>(
buyer: &signer,
lister_address: address,
listing_creation_number: u64,
) acquires Config {
// charge fee for the aggregator
let config = borrow_global<Config>(@marketplace);
// buy the token from owner directly
bid_utils::buy_from_owner_with_fee<CoinType>(
buyer,
lister_address,
listing_creation_number,
config.fee_address,
config.market_fee_numerator,
config.market_fee_denominator,
);
}
}
/// An marketplace library providing basic function for listing NFTs
/// To see how to use the library, please check the two example contract in the same folder
module marketplace::marketplace_listing_utils {
use std::error;
use std::signer;
use std::string::String;
use aptos_framework::account;
use aptos_framework::event::{Self, EventHandle};
use aptos_std::table::{Self, Table};
use aptos_std::guid::{Self, ID};
use aptos_token::token::{Self, TokenId, WithdrawCapability};
use aptos_token::property_map::{Self, PropertyMap};
friend marketplace::marketplace_bid_utils;
//
// Errors
//
/// Not enough token to list
const EOWNER_NOT_HAVING_ENOUGH_TOKEN: u64 = 1;
/// Listing doesn't exist
const ELISTING_NOT_EXIST:u64 = 2;
/// Withdraw time should be longer than listing time
const EWITHDRAW_EXPIRE_TIME_SHORT_THAN_LISTING_TIME: u64 = 3;
/// Start time should be less than expire time
const ESTART_TIME_LARGER_THAN_EXPIRE_TIME: u64 = 4;
/// Listing zero token
const ELISTING_ZERO_TOKEN: u64 = 5;
/// immutable struct for recording listing info.
struct Listing<phantom CoinType> has drop, store {
id: ID,
token_id: TokenId,
amount: u64,
min_price: u64,
instant_sale: bool, // true for marketplace and false for auction
start_sec: u64, // timestamp in secs for the listing starting time
expiration_sec: u64, // timestamp in secs for the listing expiration time
withdraw_cap: WithdrawCapability,
config: PropertyMap,
}
struct ListingEvent has copy, drop, store {
id: ID,
token_id: TokenId,
amount: u64,
min_price: u64,
instant_sale: bool,
start_sec: u64,
expiration_sec: u64,
withdraw_sec: u64,
market_address: address,
config: PropertyMap,
}
struct CancelListingEvent has copy, drop, store {
id: ID,
market_address: address,
}
/// store listings on the owner's account
struct ListingRecords<phantom CoinType> has key {
records: Table<ID, Listing<CoinType>>,
listing_event: EventHandle<ListingEvent>,
cancel_listing_event: EventHandle<CancelListingEvent>,
}
//
// entry functions
//
/// creator uses this function to directly list token for sale under their own accounts
public entry fun direct_listing<CoinType>(
owner: &signer,
creator: address,
collection_name: String,
token_name: String,
property_version: u64,
amount: u64,
min_price: u64,
instant_sale: bool, // indicate if this listing is for sale or for auction
start_sec: u64,
expiration_sec: u64,
withdraw_expiration_sec: u64,
) acquires ListingRecords {
let token_id = token::create_token_id_raw(creator, collection_name, token_name, property_version);
create_list_under_user_account<CoinType>(
owner,
token_id,
amount,
min_price,
instant_sale,
start_sec,
expiration_sec,
withdraw_expiration_sec,
);
}
/// remove a listing for the direct listing records
public entry fun cancel_direct_listing<CoinType>(
owner: &signer,
listing_id_creation_number: u64
) acquires ListingRecords {
let listing_id = guid::create_id(signer::address_of(owner), listing_id_creation_number);
let owner_addr = signer::address_of(owner);
let records = borrow_global_mut<ListingRecords<CoinType>>(owner_addr);
assert!(table::contains(&records.records, listing_id), error::not_found(ELISTING_NOT_EXIST));
table::remove(&mut records.records, listing_id);
event::emit_event<CancelListingEvent>(
&mut records.cancel_listing_event,
CancelListingEvent {
id: listing_id,
market_address: signer::address_of(owner),
},
);
}
//
// public functions
//
/// Return a listing struct, marketplace owner can use this function to create a listing and store it in its inventory
public fun create_listing<CoinType>(
owner: &signer,
token_id: TokenId,
amount: u64,
min_price: u64,
instant_sale: bool,
start_sec: u64,
listing_expiration_sec: u64,
withdraw_expiration_sec: u64, // The end time when the listed token can be withdrawn.
keys: vector<String>,
values: vector<vector<u8>>,
types: vector<String>,
): Listing<CoinType> {
let owner_addr = signer::address_of(owner);
assert!(listing_expiration_sec > start_sec, error::invalid_argument(ESTART_TIME_LARGER_THAN_EXPIRE_TIME));
assert!(token::balance_of(owner_addr, token_id) >= amount, error::invalid_argument(EOWNER_NOT_HAVING_ENOUGH_TOKEN));
assert!(withdraw_expiration_sec > listing_expiration_sec, error::invalid_argument(EWITHDRAW_EXPIRE_TIME_SHORT_THAN_LISTING_TIME));
assert!(amount > 0, error::invalid_argument(ELISTING_ZERO_TOKEN));
Listing<CoinType> {
id: create_listing_id(owner),
token_id,
amount,
min_price,
instant_sale,
start_sec,
expiration_sec: listing_expiration_sec,
withdraw_cap: token::create_withdraw_capability(owner, token_id, amount, withdraw_expiration_sec),
config: property_map::new(keys, values, types),
}
}
public fun initialize_listing_records<CoinType>(owner: &signer){
let owner_addr = signer::address_of(owner);
if (!exists<ListingRecords<CoinType>>(owner_addr)) {
move_to(
owner,
ListingRecords<CoinType> {
records: table::new(),
listing_event: account::new_event_handle<ListingEvent>(owner),
cancel_listing_event: account::new_event_handle<CancelListingEvent>(owner),
}
);
};
}
public fun create_list_under_user_account<CoinType>(
owner: &signer,
token_id: TokenId,
amount: u64,
min_price: u64,
instant_sale: bool,
start_sec: u64,
expiration_sec: u64,
withdraw_expiration_sec: u64,
): ID acquires ListingRecords {
let owner_addr = signer::address_of(owner);
let record = create_listing<CoinType>(
owner,
token_id,
amount,
min_price,
instant_sale,
start_sec,
expiration_sec,
withdraw_expiration_sec,
vector<String>[],
vector<vector<u8>>[],
vector<String>[],
);
initialize_listing_records<CoinType>(owner);
let records = borrow_global_mut<ListingRecords<CoinType>>(owner_addr);
let id = create_listing_id(owner);
// add a new record to the listing
table::add(&mut records.records, id, record);
event::emit_event<ListingEvent>(
&mut records.listing_event,
ListingEvent {
id,
token_id,
amount,
min_price,
instant_sale,
start_sec,
expiration_sec,
withdraw_sec: withdraw_expiration_sec,
market_address: owner_addr,
config: property_map::empty(),
},
);
id
}
public fun destroy_listing<CoinType>(entry: Listing<CoinType>): (
ID,
TokenId,
u64,
u64,
bool,
u64,
u64,
WithdrawCapability,
PropertyMap,
){
let Listing {
id,
token_id,
amount,
min_price,
instant_sale,
start_sec,
expiration_sec,
withdraw_cap,
config,
} = entry;
(id, token_id, amount, min_price, instant_sale, start_sec, expiration_sec, withdraw_cap, config)
}
/// util function for constructing the listing id from raw fields
public fun create_listing_id_raw(lister: address, listing_creation_number: u64): ID {
guid::create_id(lister, listing_creation_number)
}
public fun get_listing_id<CoinType>(list: &Listing<CoinType>): ID {
list.id
}
public fun get_listing_id_tuple<CoinType>(list: &Listing<CoinType>): (u64, address) {
let id = list.id;
(guid::id_creation_num(&id), guid::id_creator_address(&id))
}
public fun get_listing_creator<CoinType>(list: &Listing<CoinType>): address {
guid::id_creator_address(&list.id)
}
public fun get_listing_token_id<CoinType>(list: &Listing<CoinType>): TokenId {
list.token_id
}
public fun get_listing_expiration<CoinType>(list: &Listing<CoinType>): u64 {
list.expiration_sec
}
public fun get_listing_start<CoinType>(list: &Listing<CoinType>): u64 {
list.start_sec
}
public fun get_listing_min_price<CoinType>(list: &Listing<CoinType>): u64 {
list.min_price
}
public fun get_listing_token_amount<CoinType>(list: &Listing<CoinType>): u64 {
list.amount
}
public fun get_listing_instant_sale<CoinType>(list: &Listing<CoinType>): bool {
list.instant_sale
}
public fun create_listing_event(
id: ID,
token_id: TokenId,
amount: u64,
min_price: u64,
instant_sale: bool,
start_sec: u64,
expiration_sec: u64,
withdraw_sec: u64,
market_address: address,
config: PropertyMap
): ListingEvent {
ListingEvent {
id, token_id, amount, min_price, instant_sale, start_sec, expiration_sec, withdraw_sec, market_address, config
}
}
/// Get the read-only listing reference from listing stored on user account.
public fun get_listing_info<CoinType>(
lister_address: address,
listing_creation_number: u64
): (TokenId, u64, u64, bool, u64, u64) acquires ListingRecords {
let listing_id = guid::create_id(lister_address, listing_creation_number);
let records = borrow_global_mut<ListingRecords<CoinType>>(lister_address);
assert!(table::contains(&records.records, listing_id), error::not_found(ELISTING_NOT_EXIST));
let listing = table::borrow(&records.records, listing_id);
(
listing.token_id,
listing.amount,
listing.min_price,
listing.instant_sale,
listing.start_sec,
listing.expiration_sec,
)
}
//
// Private or friend functions
//
/// internal function for creating a new unique id for a listing
fun create_listing_id(owner: &signer): ID {
let gid = account::create_guid(owner);
guid::id(&gid)
}
/// Get the listing struct which contains withdraw_capability
/// This function should stay friend to prevent Listing be exposed to un-trusted module
public(friend) fun remove_listing<CoinType>(lister_address: address, listing_creation_number: u64): Listing<CoinType> acquires ListingRecords {
let listing_id = guid::create_id(lister_address, listing_creation_number);
let records = borrow_global_mut<ListingRecords<CoinType>>(lister_address);
assert!(table::contains(&records.records, listing_id), error::not_found(ELISTING_NOT_EXIST));
table::remove(&mut records.records, listing_id)
}
#[test(owner = @marketplace)]
public fun test_cancel_listing(owner: signer)acquires ListingRecords {
use aptos_framework::coin;
account::create_account_for_test(signer::address_of(&owner));
let token_id = token::create_collection_and_token(
&owner,
1,
2,
1,
vector<String>[],
vector<vector<u8>>[],
vector<String>[],
vector<bool>[false, false, false],
vector<bool>[false, false, false, false, false],
);
let listing_id = create_list_under_user_account<coin::FakeMoney>(
&owner,
token_id,
1,
1,
false,
0,
10000,
10001,
);
cancel_direct_listing<coin::FakeMoney>(&owner, guid::id_creation_num(&listing_id));
}
}
[package]
name = "Examples"
version = "0.0.0"
[addresses]
MoonCoin = "_"
[dependencies]
AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-framework/", rev = "main" }
//:!:>moon
module MoonCoin::moon_coin {
use aptos_framework::coin;
struct MoonCoin {}
fun init_module(sender: &signer) {
aptos_framework::managed_coin::initialize<MoonCoin>(
sender,
b"Moon Coin",
b"MOON",
6,
false,
);
}
public entry fun register(account: &signer) {
aptos_framework::managed_coin::register<MoonCoin>(account)
}
public entry fun mint(account: &signer, dst_addr: address, amount: u64) {
aptos_framework::managed_coin::mint<MoonCoin>(account, dst_addr, amount);
}
public entry fun burn(account: &signer, amount: u64) {
aptos_framework::managed_coin::burn<MoonCoin>(account, amount);
}
public entry fun transfer(from: &signer, to: address, amount: u64,) {
coin::transfer<MoonCoin>(from, to, amount);
}
}
//<:!:moon
[package]
name = "TicketTutorial"
version = "0.1.0"
[addresses]
TicketTutorial = "0xcafe"
[dependencies]
AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-framework", rev = "testnet" }
/// The original source code is https://github.com/magnum6actual/Aptos-Tutorial
module TicketTutorial::Tickets {
use std::signer;
use std::vector;
use std::string;
use aptos_framework::coin;
use aptos_framework::aptos_coin::AptosCoin;
#[test_only]
use aptos_framework::account;
use aptos_std::table_with_length;
const ENO_VENUE: u64 = 0;
const ENO_TICKETS: u64 = 1;
const ENO_ENVELOPE: u64 = 2;
const EINVALID_TICKET_COUNT: u64 = 3;
const EINVALID_TICKET: u64 = 4;
const EINVALID_PRICE: u64 = 5;
const EMAX_SEATS: u64 = 6;
const EINVALID_BALANCE: u64 = 7;
struct SeatIdentifier has store, drop, copy {
row: string::String,
seat_number: u64,
}
struct ConcertTicket has store, drop {
identifier: SeatIdentifier,
ticket_code: string::String,
price: u64,
}
struct Theater has key {
available_tickets: table_with_length::TableWithLength<SeatIdentifier, ConcertTicket>,
max_seats: u64,
}
struct TicketEnvelope has key {
tickets: vector<ConcertTicket>,
}
public entry fun init_theater(vanue_owner: &signer, max_seats: u64) {
let available_tickets = table_with_length::new<SeatIdentifier, ConcertTicket>();
move_to<Theater>(vanue_owner, Theater {available_tickets, max_seats});
}
public entry fun create_ticket(
seller: &signer,
row: string::String,
seat_number: u64,
ticket_code: string::String,
price: u64,
) acquires Theater {
let seller_addr = signer::address_of(seller);
assert!(exists<Theater>(seller_addr), ENO_VENUE);
let current_seat_count = available_ticket_count(seller_addr);
let theater = borrow_global_mut<Theater>(seller_addr);
assert!(current_seat_count < theater.max_seats, EMAX_SEATS);
let identifier = SeatIdentifier { row, seat_number };
let ticket = ConcertTicket {
identifier,
ticket_code,
price,
};
table_with_length::add(&mut theater.available_tickets, identifier, ticket)
}
public entry fun purchase_ticket(buyer: &signer, seller_addr: address, row: string::String, seat_number: u64) acquires Theater, TicketEnvelope {
let buyer_addr = signer::address_of(buyer);
let target_seat_id = SeatIdentifier { row, seat_number };
let theater = borrow_global_mut<Theater>(seller_addr);
assert!(table_with_length::contains(&theater.available_tickets, target_seat_id), EINVALID_TICKET);
let target_ticket = table_with_length::borrow(&theater.available_tickets, target_seat_id);
coin::transfer<AptosCoin>(buyer, seller_addr, target_ticket.price);
let ticket = table_with_length::remove(&mut theater.available_tickets, target_seat_id);
if (!exists<TicketEnvelope>(buyer_addr)) {
move_to(buyer, TicketEnvelope{tickets: vector::empty()});
};
let envelop = borrow_global_mut<TicketEnvelope>(buyer_addr);
vector::push_back(&mut envelop.tickets, ticket);
}
#[view]
public fun available_ticket_count(seller_addr: address): u64 acquires Theater
{
let theater = borrow_global<Theater>(seller_addr);
table_with_length::length<SeatIdentifier, ConcertTicket>(&theater.available_tickets)
}
#[test(seller = @0x3, buyer = @0x2, aptos_framework = @aptos_framework)]
public entry fun sender_can_buy_ticket(seller: signer, buyer: signer, aptos_framework: &signer) acquires Theater, TicketEnvelope
{
let seller_addr = signer::address_of(&seller);
// initialize the theater
init_theater(&seller, 3);
assert!(exists<Theater>(seller_addr), ENO_VENUE);
// create some tickets
create_ticket(&seller, string::utf8(b"A"), 24, string::utf8(b"AB43C7F"), 15);
create_ticket(&seller, string::utf8(b"A"), 25, string::utf8(b"AB43CFD"), 15);
create_ticket(&seller, string::utf8(b"A"), 26, string::utf8(b"AB13C7F"), 20);
// verify we have 3 tickets now
assert!(available_ticket_count(seller_addr) == 3, EINVALID_TICKET_COUNT);
// initialize & fund account to buy tickets
account::create_account_for_test(signer::address_of(&seller));
account::create_account_for_test(signer::address_of(&buyer));
let (burn_cap, mint_cap) = aptos_framework::aptos_coin::initialize_for_test(aptos_framework);
coin::register<AptosCoin>(&seller);
coin::register<AptosCoin>(&buyer);
coin::deposit(signer::address_of(&buyer), coin::mint(100, &mint_cap));
assert!(coin::balance<AptosCoin>(signer::address_of(&buyer)) == 100, EINVALID_BALANCE);
// buy a ticket and confirm account balance changes
purchase_ticket(&buyer, seller_addr, string::utf8(b"A"), 24);
assert!(exists<TicketEnvelope>(signer::address_of(&buyer)), ENO_ENVELOPE);
assert!(coin::balance<AptosCoin>(signer::address_of(&buyer)) == 85, EINVALID_BALANCE);
assert!(coin::balance<AptosCoin>(signer::address_of(&seller)) == 15, EINVALID_BALANCE);
assert!(available_ticket_count(seller_addr) == 2, EINVALID_TICKET_COUNT);
// buy a second ticket & ensure balance has changed by 20
purchase_ticket(&buyer, seller_addr, string::utf8(b"A"), 26);
assert!(coin::balance<AptosCoin>(signer::address_of(&buyer)) == 65, EINVALID_BALANCE);
assert!(coin::balance<AptosCoin>(signer::address_of(&seller)) == 35, EINVALID_BALANCE);
coin::destroy_burn_cap(burn_cap);
coin::destroy_mint_cap(mint_cap);
}
}
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.2 <0.9.0;
/**
* @title Storage
* @dev Store & retrieve value in a variable
* @custom:dev-run-script ./scripts/deploy_with_ethers.ts
*/
contract Storage {
uint256 number;
/**
* @dev Store value in variable
* @param num value to store
*/
function store(uint256 num) public {
number = num;
}
/**
* @dev Return value
* @return value of 'number'
*/
function retrieve() public view returns (uint256){
return number;
}
}
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
import "hardhat/console.sol";
/**
* @title Owner
* @dev Set & change owner
*/
contract Owner {
address private owner;
// event for EVM logging
event OwnerSet(address indexed oldOwner, address indexed newOwner);
// modifier to check if caller is owner
modifier isOwner() {
// If the first argument of 'require' evaluates to 'false', execution terminates and all
// changes to the state and to Ether balances are reverted.
// This used to consume all gas in old EVM versions, but not anymore.
// It is often a good idea to use 'require' to check if functions are called correctly.
// As a second argument, you can also provide an explanation about what went wrong.
require(msg.sender == owner, "Caller is not owner");
_;
}
/**
* @dev Set contract deployer as owner
*/
constructor() {
console.log("Owner contract deployed by:", msg.sender);
owner = msg.sender; // 'msg.sender' is sender of current call, contract deployer for a constructor
emit OwnerSet(address(0), owner);
}
/**
* @dev Change owner
* @param newOwner address of new owner
*/
function changeOwner(address newOwner) public isOwner {
emit OwnerSet(owner, newOwner);
owner = newOwner;
}
/**
* @dev Return owner address
* @return address of owner
*/
function getOwner() external view returns (address) {
return owner;
}
}
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
/**
* @title Ballot
* @dev Implements voting process along with vote delegation
*/
contract Ballot {
struct Voter {
uint weight; // weight is accumulated by delegation
bool voted; // if true, that person already voted
address delegate; // person delegated to
uint vote; // index of the voted proposal
}
struct Proposal {
// If you can limit the length to a certain number of bytes,
// always use one of bytes1 to bytes32 because they are much cheaper
bytes32 name; // short name (up to 32 bytes)
uint voteCount; // number of accumulated votes
}
address public chairperson;
mapping(address => Voter) public voters;
Proposal[] public proposals;
/**
* @dev Create a new ballot to choose one of 'proposalNames'.
* @param proposalNames names of proposals
*/
constructor(bytes32[] memory proposalNames) {
chairperson = msg.sender;
voters[chairperson].weight = 1;
for (uint i = 0; i < proposalNames.length; i++) {
// 'Proposal({...})' creates a temporary
// Proposal object and 'proposals.push(...)'
// appends it to the end of 'proposals'.
proposals.push(Proposal({
name: proposalNames[i],
voteCount: 0
}));
}
}
/**
* @dev Give 'voter' the right to vote on this ballot. May only be called by 'chairperson'.
* @param voter address of voter
*/
function giveRightToVote(address voter) public {
require(
msg.sender == chairperson,
"Only chairperson can give right to vote."
);
require(
!voters[voter].voted,
"The voter already voted."
);
require(voters[voter].weight == 0);
voters[voter].weight = 1;
}
/**
* @dev Delegate your vote to the voter 'to'.
* @param to address to which vote is delegated
*/
function delegate(address to) public {
Voter storage sender = voters[msg.sender];
require(!sender.voted, "You already voted.");
require(to != msg.sender, "Self-delegation is disallowed.");
while (voters[to].delegate != address(0)) {
to = voters[to].delegate;
// We found a loop in the delegation, not allowed.
require(to != msg.sender, "Found loop in delegation.");
}
sender.voted = true;
sender.delegate = to;
Voter storage delegate_ = voters[to];
if (delegate_.voted) {
// If the delegate already voted,
// directly add to the number of votes
proposals[delegate_.vote].voteCount += sender.weight;
} else {
// If the delegate did not vote yet,
// add to her weight.
delegate_.weight += sender.weight;
}
}
/**
* @dev Give your vote (including votes delegated to you) to proposal 'proposals[proposal].name'.
* @param proposal index of proposal in the proposals array
*/
function vote(uint proposal) public {
Voter storage sender = voters[msg.sender];
require(sender.weight != 0, "Has no right to vote");
require(!sender.voted, "Already voted.");
sender.voted = true;
sender.vote = proposal;
// If 'proposal' is out of the range of the array,
// this will throw automatically and revert all
// changes.
proposals[proposal].voteCount += sender.weight;
}
/**
* @dev Computes the winning proposal taking all previous votes into account.
* @return winningProposal_ index of winning proposal in the proposals array
*/
function winningProposal() public view
returns (uint winningProposal_)
{
uint winningVoteCount = 0;
for (uint p = 0; p < proposals.length; p++) {
if (proposals[p].voteCount > winningVoteCount) {
winningVoteCount = proposals[p].voteCount;
winningProposal_ = p;
}
}
}
/**
* @dev Calls winningProposal() function to get the index of the winner contained in the proposals array and then
* @return winnerName_ the name of the winner
*/
function winnerName() public view
returns (bytes32 winnerName_)
{
winnerName_ = proposals[winningProposal()].name;
}
}
/// <reference types="near-sdk-as/assembly/as_types" />
import { storage, logging } from "near-sdk-as";
// Public method - Returns the counter value
export function get_num(): i8 {
logging.log(storage.getPrimitive<i8>("counter", 0));
return storage.getPrimitive<i8>("counter", 0);
}
// Public method - Increment the counter
export function increment(): void {
safeguard_overflow()
const new_value = get_num() + 1;
storage.set<i8>("counter", new_value);
logging.log("Increased number to " + new_value.toString());
}
// Public method - Decrement the counter
export function decrement(): void {
safeguard_underflow()
const new_value = get_num() - 1;
storage.set<i8>("counter", new_value);
logging.log("Decreased number to " + new_value.toString());
}
// Public method - Reset to zero
export function reset(): void {
storage.set<i8>("counter", 0);
logging.log("Reset counter to zero");
}
// Private method - Safeguard against overflow
function safeguard_overflow(): void{
const value = get_num()
assert(value < 127, "Counter is at maximum")
}
// Private method - Safeguard against underflow
function safeguard_underflow(): void{
const value = get_num()
assert(value > -128, "Counter is at minimum")
}
{
"extends": "assemblyscript/std/assembly.json",
"include": [
"./**/*.ts"
]
}
{
"plugins": [
"near-sdk-js/lib/build-tools/near-bindgen-exporter",
["@babel/plugin-proposal-decorators", {"version": "legacy"}]
],
"presets": ["@babel/preset-typescript"]
}
{
"name": "contract",
"version": "1.0.0",
"license": "(MIT AND Apache-2.0)",
"type": "module",
"scripts": {
"build": "./build.sh",
"deploy": "./deploy.sh",
"test": "echo no unit testing"
},
"dependencies": {
"near-cli": "^3.4.0",
"near-sdk-js": "0.5.0"
}
}
import { NearBindgen, near, call, view } from 'near-sdk-js'
@NearBindgen({})
class Counter {
val = 0;
@view({}) // Public read-only method: Returns the counter value.
get_num() {
return this.val
}
@call({}) // Public method: Increment the counter.
increment() {
this.val += 1;
near.log(`Increased number to ${this.val}`)
}
@call({}) // Public method: Decrement the counter.
decrement() {
this.val -= 1;
near.log(`Decreased number to ${this.val}`)
}
@call({}) // Public method - Reset to zero.
reset() {
this.val = 0;
near.log(`Reset counter to zero`)
}
}
// this line is added to create a gist. Empty file is not allowed.
[package]
name = "contract"
version = "1.0.0"
authors = ["Near Inc <hello@near.org>"]
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
near-sdk = "4.1.0"
uint = { version = "0.9.3", default-features = false }
[profile.release]
codegen-units = 1
opt-level = "z"
lto = true
debug = false
panic = "abort"
overflow-checks = true
[workspace]
members = []
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::{log, near_bindgen};
#[near_bindgen]
#[derive(Default, BorshDeserialize, BorshSerialize)]
pub struct Counter {
val: i8,
}
#[near_bindgen]
impl Counter {
// Public read-only method: Returns the counter value.
pub fn get_num(&self) -> i8 {
return self.val;
}
// Public method: Increment the counter.
pub fn increment(&mut self) {
self.val += 1;
log!("Increased number to {}", self.val);
}
// Public method: Decrement the counter.
pub fn decrement(&mut self) {
self.val -= 1;
log!("Decreased number to {}", self.val);
}
// Public method - Reset to zero.
pub fn reset(&mut self) {
self.val = 0;
log!("Reset counter to zero");
}
}
/*
* the rest of this file sets up unit tests
* to run these, the command will be: `cargo test`
* Note: 'rust-counter-tutorial' comes from cargo.toml's 'name' key
*/
// use the attribute below for unit tests
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn increment() {
// instantiate a contract variable with the counter at zero
let mut contract = Counter { val: 0 };
contract.increment();
assert_eq!(1, contract.get_num());
}
#[test]
fn decrement() {
let mut contract = Counter { val: 0 };
contract.decrement();
assert_eq!(-1, contract.get_num());
}
#[test]
fn increment_and_reset() {
let mut contract = Counter { val: 0 };
contract.increment();
contract.reset();
assert_eq!(0, contract.get_num());
}
#[test]
#[should_panic]
fn panics_on_overflow() {
let mut contract = Counter { val: 127 };
contract.increment();
}
#[test]
#[should_panic]
fn panics_on_underflow() {
let mut contract = Counter { val: -128 };
contract.decrement();
}
}
[package]
name = "fungible-token"
version = "1.1.0"
authors = ["Near Inc <hello@nearprotocol.com>"]
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
near-sdk = "4.1.0"
near-contract-standards = "4.1.0"
schemars = "0.8"
/*!
Fungible Token implementation with JSON serialization.
NOTES:
- The maximum balance value is limited by U128 (2**128 - 1).
- JSON calls should pass U128 as a base-10 string. E.g. "100".
- The contract optimizes the inner trie structure by hashing account IDs. It will prevent some
abuse of deep tries. Shouldn't be an issue, once NEAR clients implement full hashing of keys.
- The contract tracks the change in storage before and after the call. If the storage increases,
the contract requires the caller of the contract to attach enough deposit to the function call
to cover the storage cost.
This is done to prevent a denial of service attack on the contract by taking all available storage.
If the storage decreases, the contract will issue a refund for the cost of the released storage.
The unused tokens from the attached deposit are also refunded, so it's safe to
attach more deposit than required.
- To prevent the deployed contract from being modified or deleted, it should not have any access
keys on its account.
*/
use near_contract_standards::fungible_token::metadata::{
FungibleTokenMetadata, FungibleTokenMetadataProvider, FT_METADATA_SPEC,
};
use near_contract_standards::fungible_token::FungibleToken;
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::collections::LazyOption;
use near_sdk::json_types::U128;
use near_sdk::{
env, log, near_bindgen, require, AccountId, Balance, BorshStorageKey, PanicOnDefault,
PromiseOrValue,
};
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct Contract {
token: FungibleToken,
metadata: LazyOption<FungibleTokenMetadata>,
}
const DATA_IMAGE_SVG_NEAR_ICON: &str = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 288 288'%3E%3Cg id='l' data-name='l'%3E%3Cpath d='M187.58,79.81l-30.1,44.69a3.2,3.2,0,0,0,4.75,4.2L191.86,103a1.2,1.2,0,0,1,2,.91v80.46a1.2,1.2,0,0,1-2.12.77L102.18,77.93A15.35,15.35,0,0,0,90.47,72.5H87.34A15.34,15.34,0,0,0,72,87.84V201.16A15.34,15.34,0,0,0,87.34,216.5h0a15.35,15.35,0,0,0,13.08-7.31l30.1-44.69a3.2,3.2,0,0,0-4.75-4.2L96.14,186a1.2,1.2,0,0,1-2-.91V104.61a1.2,1.2,0,0,1,2.12-.77l89.55,107.23a15.35,15.35,0,0,0,11.71,5.43h3.13A15.34,15.34,0,0,0,216,201.16V87.84A15.34,15.34,0,0,0,200.66,72.5h0A15.35,15.35,0,0,0,187.58,79.81Z'/%3E%3C/g%3E%3C/svg%3E";
#[derive(BorshSerialize, BorshStorageKey)]
enum StorageKey {
FungibleToken,
Metadata,
}
#[near_bindgen]
impl Contract {
/// Initializes the contract with the given total supply owned by the given `owner_id` with
/// default metadata (for example purposes only).
#[init]
pub fn new_default_meta(owner_id: AccountId, total_supply: U128) -> Self {
Self::new(
owner_id,
total_supply,
FungibleTokenMetadata {
spec: FT_METADATA_SPEC.to_string(),
name: "Example NEAR fungible token".to_string(),
symbol: "EXAMPLE".to_string(),
icon: Some(DATA_IMAGE_SVG_NEAR_ICON.to_string()),
reference: None,
reference_hash: None,
decimals: 24,
},
)
}
/// Initializes the contract with the given total supply owned by the given `owner_id` with
/// the given fungible token metadata.
#[init]
pub fn new(owner_id: AccountId, total_supply: U128, metadata: FungibleTokenMetadata) -> Self {
require!(!env::state_exists(), "Already initialized");
metadata.assert_valid();
let mut this = Self {
token: FungibleToken::new(StorageKey::FungibleToken),
metadata: LazyOption::new(StorageKey::Metadata, Some(&metadata)),
};
this.token.internal_register_account(&owner_id);
this.token.internal_deposit(&owner_id, total_supply.into());
this
}
fn on_account_closed(&mut self, account_id: AccountId, balance: Balance) {
log!("Closed @{} with {}", account_id, balance);
}
fn on_tokens_burned(&mut self, account_id: AccountId, amount: Balance) {
log!("Account @{} burned {}", account_id, amount);
}
}
near_contract_standards::impl_fungible_token_core!(Contract, token, on_tokens_burned);
near_contract_standards::impl_fungible_token_storage!(Contract, token, on_account_closed);
#[near_bindgen]
impl FungibleTokenMetadataProvider for Contract {
fn ft_metadata(&self) -> FungibleTokenMetadata {
self.metadata.get().unwrap()
}
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use near_sdk::test_utils::{accounts, VMContextBuilder};
use near_sdk::{testing_env, Balance};
use super::*;
const TOTAL_SUPPLY: Balance = 1_000_000_000_000_000;
fn get_context(predecessor_account_id: AccountId) -> VMContextBuilder {
let mut builder = VMContextBuilder::new();
builder
.current_account_id(accounts(0))
.signer_account_id(predecessor_account_id.clone())
.predecessor_account_id(predecessor_account_id);
builder
}
#[test]
fn test_new() {
let mut context = get_context(accounts(1));
testing_env!(context.build());
let contract = Contract::new_default_meta(accounts(1).into(), TOTAL_SUPPLY.into());
testing_env!(context.is_view(true).build());
assert_eq!(contract.ft_total_supply().0, TOTAL_SUPPLY);
assert_eq!(contract.ft_balance_of(accounts(1)).0, TOTAL_SUPPLY);
}
#[test]
#[should_panic(expected = "The contract is not initialized")]
fn test_default() {
let context = get_context(accounts(1));
testing_env!(context.build());
let _contract = Contract::default();
}
#[test]
fn test_transfer() {
let mut context = get_context(accounts(2));
testing_env!(context.build());
let mut contract = Contract::new_default_meta(accounts(2).into(), TOTAL_SUPPLY.into());
testing_env!(context
.storage_usage(env::storage_usage())
.attached_deposit(contract.storage_balance_bounds().min.into())
.predecessor_account_id(accounts(1))
.build());
// Paying for account registration, aka storage deposit
contract.storage_deposit(None, None);
testing_env!(context
.storage_usage(env::storage_usage())
.attached_deposit(1)
.predecessor_account_id(accounts(2))
.build());
let transfer_amount = TOTAL_SUPPLY / 3;
contract.ft_transfer(accounts(1), transfer_amount.into(), None);
testing_env!(context
.storage_usage(env::storage_usage())
.account_balance(env::account_balance())
.is_view(true)
.attached_deposit(0)
.build());
assert_eq!(contract.ft_balance_of(accounts(2)).0, (TOTAL_SUPPLY - transfer_amount));
assert_eq!(contract.ft_balance_of(accounts(1)).0, transfer_amount);
}
}
[package]
name = "non-fungible-token"
version = "1.1.0"
authors = ["Near Inc <hello@nearprotocol.com>"]
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
near-sdk = "4.1.0"
near-contract-standards = "4.1.0"
/*!
Non-Fungible Token implementation with JSON serialization.
NOTES:
- The maximum balance value is limited by U128 (2**128 - 1).
- JSON calls should pass U128 as a base-10 string. E.g. "100".
- The contract optimizes the inner trie structure by hashing account IDs. It will prevent some
abuse of deep tries. Shouldn't be an issue, once NEAR clients implement full hashing of keys.
- The contract tracks the change in storage before and after the call. If the storage increases,
the contract requires the caller of the contract to attach enough deposit to the function call
to cover the storage cost.
This is done to prevent a denial of service attack on the contract by taking all available storage.
If the storage decreases, the contract will issue a refund for the cost of the released storage.
The unused tokens from the attached deposit are also refunded, so it's safe to
attach more deposit than required.
- To prevent the deployed contract from being modified or deleted, it should not have any access
keys on its account.
*/
use near_contract_standards::non_fungible_token::metadata::{
NFTContractMetadata, NonFungibleTokenMetadataProvider, TokenMetadata, NFT_METADATA_SPEC,
};
use near_contract_standards::non_fungible_token::NonFungibleToken;
use near_contract_standards::non_fungible_token::{Token, TokenId};
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::collections::LazyOption;
use near_sdk::{
env, near_bindgen, require, AccountId, BorshStorageKey, PanicOnDefault, Promise, PromiseOrValue,
};
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct Contract {
tokens: NonFungibleToken,
metadata: LazyOption<NFTContractMetadata>,
}
const DATA_IMAGE_SVG_NEAR_ICON: &str = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 288 288'%3E%3Cg id='l' data-name='l'%3E%3Cpath d='M187.58,79.81l-30.1,44.69a3.2,3.2,0,0,0,4.75,4.2L191.86,103a1.2,1.2,0,0,1,2,.91v80.46a1.2,1.2,0,0,1-2.12.77L102.18,77.93A15.35,15.35,0,0,0,90.47,72.5H87.34A15.34,15.34,0,0,0,72,87.84V201.16A15.34,15.34,0,0,0,87.34,216.5h0a15.35,15.35,0,0,0,13.08-7.31l30.1-44.69a3.2,3.2,0,0,0-4.75-4.2L96.14,186a1.2,1.2,0,0,1-2-.91V104.61a1.2,1.2,0,0,1,2.12-.77l89.55,107.23a15.35,15.35,0,0,0,11.71,5.43h3.13A15.34,15.34,0,0,0,216,201.16V87.84A15.34,15.34,0,0,0,200.66,72.5h0A15.35,15.35,0,0,0,187.58,79.81Z'/%3E%3C/g%3E%3C/svg%3E";
#[derive(BorshSerialize, BorshStorageKey)]
enum StorageKey {
NonFungibleToken,
Metadata,
TokenMetadata,
Enumeration,
Approval,
}
#[near_bindgen]
impl Contract {
/// Initializes the contract owned by `owner_id` with
/// default metadata (for example purposes only).
#[init]
pub fn new_default_meta(owner_id: AccountId) -> Self {
Self::new(
owner_id,
NFTContractMetadata {
spec: NFT_METADATA_SPEC.to_string(),
name: "Example NEAR non-fungible token".to_string(),
symbol: "EXAMPLE".to_string(),
icon: Some(DATA_IMAGE_SVG_NEAR_ICON.to_string()),
base_uri: None,
reference: None,
reference_hash: None,
},
)
}
#[init]
pub fn new(owner_id: AccountId, metadata: NFTContractMetadata) -> Self {
require!(!env::state_exists(), "Already initialized");
metadata.assert_valid();
Self {
tokens: NonFungibleToken::new(
StorageKey::NonFungibleToken,
owner_id,
Some(StorageKey::TokenMetadata),
Some(StorageKey::Enumeration),
Some(StorageKey::Approval),
),
metadata: LazyOption::new(StorageKey::Metadata, Some(&metadata)),
}
}
/// Mint a new token with ID=`token_id` belonging to `token_owner_id`.
///
/// Since this example implements metadata, it also requires per-token metadata to be provided
/// in this call. `self.tokens.mint` will also require it to be Some, since
/// `StorageKey::TokenMetadata` was provided at initialization.
///
/// `self.tokens.mint` will enforce `predecessor_account_id` to equal the `owner_id` given in
/// initialization call to `new`.
#[payable]
pub fn nft_mint(
&mut self,
token_id: TokenId,
token_owner_id: AccountId,
token_metadata: TokenMetadata,
) -> Token {
assert_eq!(env::predecessor_account_id(), self.tokens.owner_id, "Unauthorized");
self.tokens.internal_mint(token_id, token_owner_id, Some(token_metadata))
}
}
near_contract_standards::impl_non_fungible_token_core!(Contract, tokens);
near_contract_standards::impl_non_fungible_token_approval!(Contract, tokens);
near_contract_standards::impl_non_fungible_token_enumeration!(Contract, tokens);
#[near_bindgen]
impl NonFungibleTokenMetadataProvider for Contract {
fn nft_metadata(&self) -> NFTContractMetadata {
self.metadata.get().unwrap()
}
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use near_sdk::test_utils::{accounts, VMContextBuilder};
use near_sdk::testing_env;
use std::collections::HashMap;
use super::*;
const MINT_STORAGE_COST: u128 = 5870000000000000000000;
fn get_context(predecessor_account_id: AccountId) -> VMContextBuilder {
let mut builder = VMContextBuilder::new();
builder
.current_account_id(accounts(0))
.signer_account_id(predecessor_account_id.clone())
.predecessor_account_id(predecessor_account_id);
builder
}
fn sample_token_metadata() -> TokenMetadata {
TokenMetadata {
title: Some("Olympus Mons".into()),
description: Some("The tallest mountain in the charted solar system".into()),
media: None,
media_hash: None,
copies: Some(1u64),
issued_at: None,
expires_at: None,
starts_at: None,
updated_at: None,
extra: None,
reference: None,
reference_hash: None,
}
}
#[test]
fn test_new() {
let mut context = get_context(accounts(1));
testing_env!(context.build());
let contract = Contract::new_default_meta(accounts(1).into());
testing_env!(context.is_view(true).build());
assert_eq!(contract.nft_token("1".to_string()), None);
}
#[test]
#[should_panic(expected = "The contract is not initialized")]
fn test_default() {
let context = get_context(accounts(1));
testing_env!(context.build());
let _contract = Contract::default();
}
#[test]
fn test_mint() {
let mut context = get_context(accounts(0));
testing_env!(context.build());
let mut contract = Contract::new_default_meta(accounts(0).into());
testing_env!(context
.storage_usage(env::storage_usage())
.attached_deposit(MINT_STORAGE_COST)
.predecessor_account_id(accounts(0))
.build());
let token_id = "0".to_string();
let token = contract.nft_mint(token_id.clone(), accounts(0), sample_token_metadata());
assert_eq!(token.token_id, token_id);
assert_eq!(token.owner_id, accounts(0));
assert_eq!(token.metadata.unwrap(), sample_token_metadata());
assert_eq!(token.approved_account_ids.unwrap(), HashMap::new());
}
#[test]
fn test_transfer() {
let mut context = get_context(accounts(0));
testing_env!(context.build());
let mut contract = Contract::new_default_meta(accounts(0).into());
testing_env!(context
.storage_usage(env::storage_usage())
.attached_deposit(MINT_STORAGE_COST)
.predecessor_account_id(accounts(0))
.build());
let token_id = "0".to_string();
contract.nft_mint(token_id.clone(), accounts(0), sample_token_metadata());
testing_env!(context
.storage_usage(env::storage_usage())
.attached_deposit(1)
.predecessor_account_id(accounts(0))
.build());
contract.nft_transfer(accounts(1), token_id.clone(), None, None);
testing_env!(context
.storage_usage(env::storage_usage())
.account_balance(env::account_balance())
.is_view(true)
.attached_deposit(0)
.build());
if let Some(token) = contract.nft_token(token_id.clone()) {
assert_eq!(token.token_id, token_id);
assert_eq!(token.owner_id, accounts(1));
assert_eq!(token.metadata.unwrap(), sample_token_metadata());
assert_eq!(token.approved_account_ids.unwrap(), HashMap::new());
} else {
panic!("token not correctly created, or not found by nft_token");
}
}
#[test]
fn test_approve() {
let mut context = get_context(accounts(0));
testing_env!(context.build());
let mut contract = Contract::new_default_meta(accounts(0).into());
testing_env!(context
.storage_usage(env::storage_usage())
.attached_deposit(MINT_STORAGE_COST)
.predecessor_account_id(accounts(0))
.build());
let token_id = "0".to_string();
contract.nft_mint(token_id.clone(), accounts(0), sample_token_metadata());
// alice approves bob
testing_env!(context
.storage_usage(env::storage_usage())
.attached_deposit(150000000000000000000)
.predecessor_account_id(accounts(0))
.build());
contract.nft_approve(token_id.clone(), accounts(1), None);
testing_env!(context
.storage_usage(env::storage_usage())
.account_balance(env::account_balance())
.is_view(true)
.attached_deposit(0)
.build());
assert!(contract.nft_is_approved(token_id.clone(), accounts(1), Some(1)));
}
#[test]
fn test_revoke() {
let mut context = get_context(accounts(0));
testing_env!(context.build());
let mut contract = Contract::new_default_meta(accounts(0).into());
testing_env!(context
.storage_usage(env::storage_usage())
.attached_deposit(MINT_STORAGE_COST)
.predecessor_account_id(accounts(0))
.build());
let token_id = "0".to_string();
contract.nft_mint(token_id.clone(), accounts(0), sample_token_metadata());
// alice approves bob
testing_env!(context
.storage_usage(env::storage_usage())
.attached_deposit(150000000000000000000)
.predecessor_account_id(accounts(0))
.build());
contract.nft_approve(token_id.clone(), accounts(1), None);
// alice revokes bob
testing_env!(context
.storage_usage(env::storage_usage())
.attached_deposit(1)
.predecessor_account_id(accounts(0))
.build());
contract.nft_revoke(token_id.clone(), accounts(1));
testing_env!(context
.storage_usage(env::storage_usage())
.account_balance(env::account_balance())
.is_view(true)
.attached_deposit(0)
.build());
assert!(!contract.nft_is_approved(token_id.clone(), accounts(1), None));
}
#[test]
fn test_revoke_all() {
let mut context = get_context(accounts(0));
testing_env!(context.build());
let mut contract = Contract::new_default_meta(accounts(0).into());
testing_env!(context
.storage_usage(env::storage_usage())
.attached_deposit(MINT_STORAGE_COST)
.predecessor_account_id(accounts(0))
.build());
let token_id = "0".to_string();
contract.nft_mint(token_id.clone(), accounts(0), sample_token_metadata());
// alice approves bob
testing_env!(context
.storage_usage(env::storage_usage())
.attached_deposit(150000000000000000000)
.predecessor_account_id(accounts(0))
.build());
contract.nft_approve(token_id.clone(), accounts(1), None);
// alice revokes bob
testing_env!(context
.storage_usage(env::storage_usage())
.attached_deposit(1)
.predecessor_account_id(accounts(0))
.build());
contract.nft_revoke_all(token_id.clone());
testing_env!(context
.storage_usage(env::storage_usage())
.account_balance(env::account_balance())
.is_view(true)
.attached_deposit(0)
.build());
assert!(!contract.nft_is_approved(token_id.clone(), accounts(1), Some(1)));
}
}
{
"plugins": [
"near-sdk-js/lib/build-tools/near-bindgen-exporter",
["@babel/plugin-proposal-decorators", {"version": "legacy"}]
],
"presets": ["@babel/preset-typescript"]
}
{
"name": "contract",
"version": "1.0.0",
"license": "(MIT AND Apache-2.0)",
"type": "module",
"scripts": {
"build": "./build.sh",
"deploy": "./deploy.sh",
"test": "echo no unit testing"
},
"dependencies": {
"near-cli": "^3.4.0",
"near-sdk-js": "0.5.0"
},
"devDependencies": {
"typescript": "^4.7.4"
}
}
import { NearBindgen, near, call, view } from 'near-sdk-js'
@NearBindgen({})
class Counter {
val: number = 0;
@view({}) // Public read-only method: Returns the counter value.
get_num(): number {
return this.val
}
@call({}) // Public method: Increment the counter.
increment() {
this.val += 1;
near.log(`Increased number to ${this.val}`)
}
@call({}) // Public method: Decrement the counter.
decrement() {
this.val -= 1;
near.log(`Decreased number to ${this.val}`)
}
@call({}) // Public method - Reset to zero.
reset() {
this.val = 0;
near.log(`Reset counter to zero`)
}
}
{
"compilerOptions": {
"experimentalDecorators": true,
"target": "es5",
"noEmit": true
},
"exclude": ["node_modules"]
}
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "ahash"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
dependencies = [
"getrandom",
"once_cell",
"version_check",
]
[[package]]
name = "anyhow"
version = "1.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
[[package]]
name = "base16ct"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce"
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64ct"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "block-buffer"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
dependencies = [
"generic-array",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "byteorder"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bytes"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "const-oid"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6340df57935414636969091153f35f68d9f00bbc8fb4a9c6054706c213e6c6bc"
[[package]]
name = "cosmwasm-crypto"
version = "1.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb64554a91d6a9231127f4355d351130a0b94e663d5d9dc8b3a54ca17d83de49"
dependencies = [
"digest 0.10.7",
"ed25519-zebra",
"k256",
"rand_core 0.6.4",
"thiserror",
]
[[package]]
name = "cosmwasm-derive"
version = "1.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0fb2ce09f41a3dae1a234d56a9988f9aff4c76441cd50ef1ee9a4f20415b028"
dependencies = [
"syn 1.0.109",
]
[[package]]
name = "cosmwasm-schema"
version = "1.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "230e5d1cefae5331db8934763c81b9c871db6a2cd899056a5694fa71d292c815"
dependencies = [
"cosmwasm-schema-derive",
"schemars",
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "cosmwasm-schema-derive"
version = "1.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43dadf7c23406cb28079d69e6cb922c9c29b9157b0fe887e3b79c783b7d4bcb8"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "cosmwasm-std"
version = "1.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4337eef8dfaf8572fe6b6b415d6ec25f9308c7bb09f2da63789209fb131363be"
dependencies = [
"base64",
"cosmwasm-crypto",
"cosmwasm-derive",
"derivative",
"forward_ref",
"hex",
"schemars",
"serde",
"serde-json-wasm",
"sha2 0.10.7",
"thiserror",
"uint",
]
[[package]]
name = "cosmwasm-storage"
version = "1.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8601d284db8776e39fe99b3416516c5636ca73cef14666b7bb9648ca32c4b89"
dependencies = [
"cosmwasm-std",
"serde",
]
[[package]]
name = "cpufeatures"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1"
dependencies = [
"libc",
]
[[package]]
name = "crunchy"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "crypto-bigint"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef"
dependencies = [
"generic-array",
"rand_core 0.6.4",
"subtle",
"zeroize",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "curve25519-dalek"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61"
dependencies = [
"byteorder",
"digest 0.9.0",
"rand_core 0.5.1",
"subtle",
"zeroize",
]
[[package]]
name = "cw-multi-test"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3f9a8ab7c3c29ec93cb7a39ce4b14a05e053153b4a17ef7cf2246af1b7c087e"
dependencies = [
"anyhow",
"cosmwasm-std",
"cosmwasm-storage",
"cw-storage-plus",
"cw-utils",
"derivative",
"itertools",
"prost",
"schemars",
"serde",
"thiserror",
]
[[package]]
name = "cw-storage-plus"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "648b1507290bbc03a8d88463d7cd9b04b1fa0155e5eef366c4fa052b9caaac7a"
dependencies = [
"cosmwasm-std",
"schemars",
"serde",
]
[[package]]
name = "cw-utils"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dbaecb78c8e8abfd6b4258c7f4fbeb5c49a5e45ee4d910d3240ee8e1d714e1b"
dependencies = [
"cosmwasm-std",
"schemars",
"serde",
"thiserror",
]
[[package]]
name = "cw2"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cf4639517490dd36b333bbd6c4fbd92e325fd0acf4683b41753bc5eb63bfc1"
dependencies = [
"cosmwasm-std",
"cw-storage-plus",
"schemars",
"serde",
]
[[package]]
name = "der"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de"
dependencies = [
"const-oid",
"zeroize",
]
[[package]]
name = "derivative"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "digest"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
dependencies = [
"generic-array",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer 0.10.4",
"crypto-common",
"subtle",
]
[[package]]
name = "dyn-clone"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30"
[[package]]
name = "ecdsa"
version = "0.14.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c"
dependencies = [
"der",
"elliptic-curve",
"rfc6979",
"signature",
]
[[package]]
name = "ed25519-zebra"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c24f403d068ad0b359e577a77f92392118be3f3c927538f2bb544a5ecd828c6"
dependencies = [
"curve25519-dalek",
"hashbrown",
"hex",
"rand_core 0.6.4",
"serde",
"sha2 0.9.9",
"zeroize",
]
[[package]]
name = "either"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
[[package]]
name = "elliptic-curve"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3"
dependencies = [
"base16ct",
"crypto-bigint",
"der",
"digest 0.10.7",
"ff",
"generic-array",
"group",
"pkcs8",
"rand_core 0.6.4",
"sec1",
"subtle",
"zeroize",
]
[[package]]
name = "ff"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160"
dependencies = [
"rand_core 0.6.4",
"subtle",
]
[[package]]
name = "forward_ref"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e"
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "group"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7"
dependencies = [
"ff",
"rand_core 0.6.4",
"subtle",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [
"ahash",
]
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest 0.10.7",
]
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a"
[[package]]
name = "k256"
version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72c1e0b51e7ec0a97369623508396067a486bd0cbed95a2659a4b863d28cfc8b"
dependencies = [
"cfg-if",
"ecdsa",
"elliptic-curve",
"sha2 0.10.7",
]
[[package]]
name = "libc"
version = "0.2.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "my-first-contract"
version = "0.1.0"
dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
"cosmwasm-storage",
"cw-multi-test",
"cw-storage-plus",
"cw2",
"schemars",
"serde",
"thiserror",
]
[[package]]
name = "once_cell"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "opaque-debug"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "pkcs8"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba"
dependencies = [
"der",
"spki",
]
[[package]]
name = "proc-macro2"
version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da"
dependencies = [
"unicode-ident",
]
[[package]]
name = "prost"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001"
dependencies = [
"bytes",
"prost-derive",
]
[[package]]
name = "prost-derive"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe"
dependencies = [
"anyhow",
"itertools",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "quote"
version = "1.0.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand_core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "rfc6979"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb"
dependencies = [
"crypto-bigint",
"hmac",
"zeroize",
]
[[package]]
name = "ryu"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9"
[[package]]
name = "schemars"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02c613288622e5f0c3fdc5dbd4db1c5fbe752746b1d1a56a0630b78fd00de44f"
dependencies = [
"dyn-clone",
"schemars_derive",
"serde",
"serde_json",
]
[[package]]
name = "schemars_derive"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "109da1e6b197438deb6db99952990c7f959572794b80ff93707d55a232545e7c"
dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 1.0.109",
]
[[package]]
name = "sec1"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928"
dependencies = [
"base16ct",
"der",
"generic-array",
"pkcs8",
"subtle",
"zeroize",
]
[[package]]
name = "serde"
version = "1.0.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde-json-wasm"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16a62a1fad1e1828b24acac8f2b468971dade7b8c3c2e672bcadefefb1f8c137"
dependencies = [
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.25",
]
[[package]]
name = "serde_derive_internals"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "serde_json"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "sha2"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
dependencies = [
"block-buffer 0.9.0",
"cfg-if",
"cpufeatures",
"digest 0.9.0",
"opaque-debug",
]
[[package]]
name = "sha2"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8"
dependencies = [
"cfg-if",
"cpufeatures",
"digest 0.10.7",
]
[[package]]
name = "signature"
version = "1.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c"
dependencies = [
"digest 0.10.7",
"rand_core 0.6.4",
]
[[package]]
name = "spki"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b"
dependencies = [
"base64ct",
"der",
]
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "subtle"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15e3fc8c0c74267e2df136e5e5fb656a464158aa57624053375eb9c8c6e25ae2"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.25",
]
[[package]]
name = "typenum"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
[[package]]
name = "uint"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52"
dependencies = [
"byteorder",
"crunchy",
"hex",
"static_assertions",
]
[[package]]
name = "unicode-ident"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "zeroize"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9"
[package]
name = "my-first-contract"
version = "0.1.0"
authors = ["0xhsy <sooyoung.hyun@dsrvlabs.com>"]
edition = "2018"
exclude = [
# Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication.
"contract.wasm",
"hash.txt",
]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib", "rlib"]
[profile.release]
opt-level = 3
debug = false
rpath = false
lto = true
debug-assertions = false
codegen-units = 1
panic = 'abort'
incremental = false
overflow-checks = true
[features]
# for more explicit tests, cargo test --features=backtraces
backtraces = ["cosmwasm-std/backtraces"]
# use library feature to disable all instantiate/execute/query exports
library = []
[dependencies]
cosmwasm-std = "1.0.0"
cosmwasm-storage = "1.0.0"
cw-storage-plus = "0.13.2"
cw2 = "0.13.2"
schemars = "0.8.8"
serde = { version = "1.0.137", default-features = false, features = ["derive"] }
thiserror = { version = "1.0.31" }
[dev-dependencies]
cosmwasm-schema = "1.0.0"
cw-multi-test = "0.13.2"
use std::env::current_dir;
use std::fs::create_dir_all;
use cosmwasm_schema::{export_schema, remove_schemas, schema_for};
use my_first_contract::msg::{ExecuteMsg, GetCountResponse, InstantiateMsg, QueryMsg};
use my_first_contract::state::State;
fn main() {
let mut out_dir = current_dir().unwrap();
out_dir.push("schema");
create_dir_all(&out_dir).unwrap();
remove_schemas(&out_dir).unwrap();
export_schema(&schema_for!(InstantiateMsg), &out_dir);
export_schema(&schema_for!(ExecuteMsg), &out_dir);
export_schema(&schema_for!(QueryMsg), &out_dir);
export_schema(&schema_for!(State), &out_dir);
export_schema(&schema_for!(GetCountResponse), &out_dir);
}
#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;
use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
use cw2::set_contract_version;
use crate::error::ContractError;
use crate::msg::{ExecuteMsg, GetCountResponse, InstantiateMsg, QueryMsg};
// use crate::msg::{ExecuteMsg, GetCountResponse, InstantiateMsg, QueryMsg, MigrateMsg};
use crate::state::{State, STATE};
// version info for migration info
const CONTRACT_NAME: &str = "crates.io:my-first-contract";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
let state = State {
count: msg.count,
owner: info.sender.clone(),
};
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
STATE.save(deps.storage, &state)?;
Ok(Response::new()
.add_attribute("method", "instantiate")
.add_attribute("owner", info.sender)
.add_attribute("count", msg.count.to_string()))
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::Increment {} => try_increment(deps),
ExecuteMsg::Reset { count } => try_reset(deps, info, count),
}
}
// #[entry_point]
// pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
// // No state migrations performed, just returned a Response
// Ok(Response::default())
// }
pub fn try_increment(deps: DepsMut) -> Result<Response, ContractError> {
STATE.update(deps.storage, |mut state| -> Result<_, ContractError> {
state.count += 1;
// state.count += 2;
Ok(state)
})?;
Ok(Response::new().add_attribute("method", "try_increment"))
}
pub fn try_reset(deps: DepsMut, info: MessageInfo, count: i32) -> Result<Response, ContractError> {
STATE.update(deps.storage, |mut state| -> Result<_, ContractError> {
if info.sender != state.owner {
return Err(ContractError::Unauthorized {});
}
state.count = count;
Ok(state)
})?;
Ok(Response::new().add_attribute("method", "reset"))
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::GetCount {} => to_binary(&query_count(deps)?),
}
}
fn query_count(deps: Deps) -> StdResult<GetCountResponse> {
let state = STATE.load(deps.storage)?;
Ok(GetCountResponse { count: state.count })
}
#[cfg(test)]
mod tests {
use super::*;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use cosmwasm_std::{coins, from_binary};
#[test]
fn proper_initialization() {
let mut deps = mock_dependencies();
let msg = InstantiateMsg { count: 17 };
let info = mock_info("creator", &coins(1000, "earth"));
// we can just call .unwrap() to assert this was a success
let res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap();
assert_eq!(0, res.messages.len());
// it worked, let's query the state
let res = query(deps.as_ref(), mock_env(), QueryMsg::GetCount {}).unwrap();
let value: GetCountResponse = from_binary(&res).unwrap();
assert_eq!(17, value.count);
}
#[test]
fn increment() {
let mut deps = mock_dependencies();
let msg = InstantiateMsg { count: 17 };
let info = mock_info("creator", &coins(2, "token"));
let _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap();
// beneficiary can release it
let info = mock_info("anyone", &coins(2, "token"));
let msg = ExecuteMsg::Increment {};
let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap();
// should increase counter by 1
let res = query(deps.as_ref(), mock_env(), QueryMsg::GetCount {}).unwrap();
let value: GetCountResponse = from_binary(&res).unwrap();
assert_eq!(18, value.count);
}
#[test]
fn reset() {
let mut deps = mock_dependencies();
let msg = InstantiateMsg { count: 17 };
let info = mock_info("creator", &coins(2, "token"));
let _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap();
// beneficiary can release it
let unauth_info = mock_info("anyone", &coins(2, "token"));
let msg = ExecuteMsg::Reset { count: 5 };
let res = execute(deps.as_mut(), mock_env(), unauth_info, msg);
match res {
Err(ContractError::Unauthorized {}) => {}
_ => panic!("Must return unauthorized error"),
}
// only the original creator can reset the counter
let auth_info = mock_info("creator", &coins(2, "token"));
let msg = ExecuteMsg::Reset { count: 5 };
let _res = execute(deps.as_mut(), mock_env(), auth_info, msg).unwrap();
// should now be 5
let res = query(deps.as_ref(), mock_env(), QueryMsg::GetCount {}).unwrap();
let value: GetCountResponse = from_binary(&res).unwrap();
assert_eq!(5, value.count);
}
}
use cosmwasm_std::StdError;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ContractError {
#[error("{0}")]
Std(#[from] StdError),
#[error("Unauthorized")]
Unauthorized {},
#[error("Custom Error val: {val:?}")]
CustomError { val: String },
// Add any other custom errors you like here.
// Look at https://docs.rs/thiserror/1.0.21/thiserror/ for details.
}
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use cosmwasm_std::{
to_binary, Addr, CosmosMsg, CustomQuery, Querier, QuerierWrapper, StdResult, WasmMsg, WasmQuery,
};
use crate::msg::{ExecuteMsg, GetCountResponse, QueryMsg};
/// CwTemplateContract is a wrapper around Addr that provides a lot of helpers
/// for working with this.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct CwTemplateContract(pub Addr);
impl CwTemplateContract {
pub fn addr(&self) -> Addr {
self.0.clone()
}
pub fn call<T: Into<ExecuteMsg>>(&self, msg: T) -> StdResult<CosmosMsg> {
let msg = to_binary(&msg.into())?;
Ok(WasmMsg::Execute {
contract_addr: self.addr().into(),
msg,
funds: vec![],
}
.into())
}
/// Get Count
pub fn count<Q, T, CQ>(&self, querier: &Q) -> StdResult<GetCountResponse>
where
Q: Querier,
T: Into<String>,
CQ: CustomQuery,
{
let msg = QueryMsg::GetCount {};
let query = WasmQuery::Smart {
contract_addr: self.addr().into(),
msg: to_binary(&msg)?,
}
.into();
let res: GetCountResponse = QuerierWrapper::<CQ>::new(querier).query(&query)?;
Ok(res)
}
}
#[cfg(test)]
mod tests {
use crate::helpers::CwTemplateContract;
use crate::msg::InstantiateMsg;
use cosmwasm_std::{Addr, Coin, Empty, Uint128};
use cw_multi_test::{App, AppBuilder, Contract, ContractWrapper, Executor};
pub fn contract_template() -> Box<dyn Contract<Empty>> {
let contract = ContractWrapper::new(
crate::contract::execute,
crate::contract::instantiate,
crate::contract::query,
);
Box::new(contract)
}
const USER: &str = "USER";
const ADMIN: &str = "ADMIN";
const NATIVE_DENOM: &str = "denom";
fn mock_app() -> App {
AppBuilder::new().build(|router, _, storage| {
router
.bank
.init_balance(
storage,
&Addr::unchecked(USER),
vec![Coin {
denom: NATIVE_DENOM.to_string(),
amount: Uint128::new(1),
}],
)
.unwrap();
})
}
fn proper_instantiate() -> (App, CwTemplateContract) {
let mut app = mock_app();
let cw_template_id = app.store_code(contract_template());
let msg = InstantiateMsg { count: 1i32 };
let cw_template_contract_addr = app
.instantiate_contract(
cw_template_id,
Addr::unchecked(ADMIN),
&msg,
&[],
"test",
None,
)
.unwrap();
let cw_template_contract = CwTemplateContract(cw_template_contract_addr);
(app, cw_template_contract)
}
mod count {
use super::*;
use crate::msg::ExecuteMsg;
#[test]
fn count() {
let (mut app, cw_template_contract) = proper_instantiate();
let msg = ExecuteMsg::Increment {};
let cosmos_msg = cw_template_contract.call(msg).unwrap();
app.execute(Addr::unchecked(USER), cosmos_msg).unwrap();
}
}
}
pub mod contract;
mod error;
pub mod helpers;
pub mod integration_tests;
pub mod msg;
pub mod state;
pub use crate::error::ContractError;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
pub count: i32,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
Increment {},
Reset { count: i32 },
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
// GetCount returns the current count as a json-encoded number
GetCount {},
}
// We define a custom struct for each query response
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct GetCountResponse {
pub count: i32,
}
// #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
// pub struct MigrateMsg {
// }
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use cosmwasm_std::Addr;
use cw_storage_plus::Item;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct State {
pub count: i32,
pub owner: Addr,
}
pub const STATE: Item<State> = Item::new("state");
[alias]
wasm = "build --release --target wasm32-unknown-unknown"
wasm-debug = "build --target wasm32-unknown-unknown"
unit-test = "test --lib --features backtraces"
integration-test = "test --test integration"
schema = "run --example schema"
[package]
name = "ibc_transfer"
version = "0.1.0"
edition = "2021"
exclude = [
# Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication.
"contract.wasm",
"hash.txt",
]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib", "rlib"]
[profile.release]
opt-level = 3
debug = false
rpath = false
lto = true
debug-assertions = false
codegen-units = 1
panic = 'abort'
incremental = false
overflow-checks = true
[features]
# for quicker tests, cargo test --lib
# for more explicit tests, cargo test --features=backtraces
backtraces = ["cosmwasm-std/backtraces"]
library = []
[dependencies]
cosmwasm-std = { version = "1.0.0", features = ["staking", "stargate"] }
cw2 = "0.15.1"
schemars = "0.8.10"
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
serde-json-wasm = { version = "0.4.1" }
cw-storage-plus = { version = "0.14.0", features = ["iterator"]}
neutron-sdk = { path = "../../packages/neutron-sdk", default-features = false, version = "0.5.0"}
protobuf = { version = "3.2.0", features = ["with-bytes"] }
[dev-dependencies]
cosmwasm-schema = { version = "1.0.0", default-features = false }
use std::env::current_dir;
use std::fs::create_dir_all;
use cosmwasm_schema::{export_schema, remove_schemas, schema_for};
use ibc_transfer::msg::{ExecuteMsg, InstantiateMsg};
fn main() {
let mut out_dir = current_dir().unwrap();
out_dir.push("schema");
create_dir_all(&out_dir).unwrap();
remove_schemas(&out_dir).unwrap();
export_schema(&schema_for!(InstantiateMsg), &out_dir);
export_schema(&schema_for!(ExecuteMsg), &out_dir);
}

Neutron IBC Transfer Example Contract

The example contract shows how to use and interact with IBC Transfer Module.

IBC transfer contract

Interacting with counterpart chain via ibc transfer is two phases process.

  1. Send ibc transfer message
  2. Accept and process ibc acknowledgement(sudo_response call)

How to test

  1. run make build in the root folder of neutron-sdk/
  2. set up Localnet
  3. cd scripts/
  4. ./test_ibc_transfer.sh (or NEUTRON_DIR=/path/to/somedir/ ./test_ibc_transfer.sh if the neutron dir is not ../../neutron/)

Checkout logs from Neutron chain: grep -E '(ibc-transfer|WASMDEBUG)' ../../neutron/data/test-1/test-1.log. You will see there debug messages from contract and neutron's ibc-transfer module itself.

Tracing ibc transfer ack(sudo)

long story short, we catch packet_sequence id in the reply handler and passthrough any payload to sudo handler using the seq_id

  1. ExecuteHandler. We save the payload we want to pass to sudo handler with a "unique-enought" id in the storage
  2. ExecuteHandler. Force submsg to replyOn::success with the msd.id we picked above
  3. ReplyHandler. In the reply handler we parse ibc packet_sequence id and map the payload to the seq_id in the storage
  4. SudoHandler. In the sudo handler we read the payload from the storage with a provided seq_id(in sudo ack packet)
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ExecuteMsg",
"oneOf": [
{
"type": "object",
"required": [
"send"
],
"properties": {
"send": {
"type": "object",
"required": [
"amount",
"channel",
"denom",
"to"
],
"properties": {
"amount": {
"type": "integer",
"format": "uint128",
"minimum": 0.0
},
"channel": {
"type": "string"
},
"denom": {
"type": "string"
},
"timeout_height": {
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 0.0
},
"to": {
"type": "string"
}
}
}
},
"additionalProperties": false
}
]
}
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "InstantiateMsg",
"type": "object"
}
use cosmwasm_std::{
coin, entry_point, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Reply, Response,
StdError, StdResult, SubMsg,
};
use cw2::set_contract_version;
use neutron_sdk::{
bindings::{
msg::{IbcFee, MsgIbcTransferResponse, NeutronMsg},
query::NeutronQuery,
},
query::min_ibc_fee::query_min_ibc_fee,
sudo::msg::{RequestPacket, RequestPacketTimeoutHeight, TransferSudoMsg},
NeutronResult,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
msg::{ExecuteMsg, InstantiateMsg, MigrateMsg},
state::{
read_reply_payload, read_sudo_payload, save_reply_payload, save_sudo_payload,
IBC_SUDO_ID_RANGE_END, IBC_SUDO_ID_RANGE_START,
},
};
// Default timeout for IbcTransfer is 10000000 blocks
const DEFAULT_TIMEOUT_HEIGHT: u64 = 10000000;
const FEE_DENOM: &str = "untrn";
const CONTRACT_NAME: &str = concat!("crates.io:neutron-sdk__", env!("CARGO_PKG_NAME"));
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
#[entry_point]
pub fn instantiate(
deps: DepsMut,
_env: Env,
_info: MessageInfo,
_msg: InstantiateMsg,
) -> StdResult<Response> {
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
Ok(Response::default())
}
#[entry_point]
pub fn execute(
deps: DepsMut<NeutronQuery>,
env: Env,
_: MessageInfo,
msg: ExecuteMsg,
) -> NeutronResult<Response<NeutronMsg>> {
match msg {
// NOTE: this is an example contract that shows how to make IBC transfers!
// Please add necessary authorization or other protection mechanisms
// if you intend to send funds over IBC
ExecuteMsg::Send {
channel,
to,
denom,
amount,
timeout_height,
} => execute_send(deps, env, channel, to, denom, amount, timeout_height),
}
}
// Example of different payload types
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct Type1 {
pub message: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct Type2 {
pub data: String,
}
// a callback handler for payload of Type1
fn sudo_callback1(deps: Deps, payload: Type1) -> StdResult<Response> {
deps.api
.debug(format!("WASMDEBUG: callback1: sudo payload: {:?}", payload).as_str());
Ok(Response::new())
}
// a callback handler for payload of Type2
fn sudo_callback2(deps: Deps, payload: Type2) -> StdResult<Response> {
deps.api
.debug(format!("WASMDEBUG: callback2: sudo payload: {:?}", payload).as_str());
Ok(Response::new())
}
// Enum representing payload to process during handling acknowledgement messages in Sudo handler
#[derive(Serialize, Deserialize)]
pub enum SudoPayload {
HandlerPayload1(Type1),
HandlerPayload2(Type2),
}
// saves payload to process later to the storage and returns a SubmitTX Cosmos SubMsg with necessary reply id
fn msg_with_sudo_callback<C: Into<CosmosMsg<T>>, T>(
deps: DepsMut<NeutronQuery>,
msg: C,
payload: SudoPayload,
) -> StdResult<SubMsg<T>> {
let id = save_reply_payload(deps.storage, payload)?;
Ok(SubMsg::reply_on_success(msg, id))
}
// prepare_sudo_payload is called from reply handler
// The method is used to extract sequence id and channel from SubmitTxResponse to process sudo payload defined in msg_with_sudo_callback later in Sudo handler.
// Such flow msg_with_sudo_callback() -> reply() -> prepare_sudo_payload() -> sudo() allows you "attach" some payload to your Transfer message
// and process this payload when an acknowledgement for the SubmitTx message is received in Sudo handler
fn prepare_sudo_payload(mut deps: DepsMut, _env: Env, msg: Reply) -> StdResult<Response> {
let payload = read_reply_payload(deps.storage, msg.id)?;
let resp: MsgIbcTransferResponse = serde_json_wasm::from_slice(
msg.result
.into_result()
.map_err(StdError::generic_err)?
.data
.ok_or_else(|| StdError::generic_err("no result"))?
.as_slice(),
)
.map_err(|e| StdError::generic_err(format!("failed to parse response: {:?}", e)))?;
let seq_id = resp.sequence_id;
let channel_id = resp.channel;
save_sudo_payload(deps.branch().storage, channel_id, seq_id, payload)?;
Ok(Response::new())
}
#[entry_point]
pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> StdResult<Response> {
match msg.id {
// It's convenient to use range of ID's to handle multiple reply messages
IBC_SUDO_ID_RANGE_START..=IBC_SUDO_ID_RANGE_END => prepare_sudo_payload(deps, env, msg),
_ => Err(StdError::generic_err(format!(
"unsupported reply message id {}",
msg.id
))),
}
}
fn execute_send(
mut deps: DepsMut<NeutronQuery>,
env: Env,
channel: String,
to: String,
denom: String,
amount: u128,
timeout_height: Option<u64>,
) -> NeutronResult<Response<NeutronMsg>> {
// contract must pay for relaying of acknowledgements
// See more info here: https://docs.neutron.org/neutron/feerefunder/overview
let fee = min_ntrn_ibc_fee(query_min_ibc_fee(deps.as_ref())?.min_fee);
let coin1 = coin(amount, denom.clone());
let msg1 = NeutronMsg::IbcTransfer {
source_port: "transfer".to_string(),
source_channel: channel.clone(),
sender: env.contract.address.to_string(),
receiver: to.clone(),
token: coin1,
timeout_height: RequestPacketTimeoutHeight {
revision_number: Some(2),
revision_height: timeout_height.or(Some(DEFAULT_TIMEOUT_HEIGHT)),
},
timeout_timestamp: 0,
memo: "".to_string(),
fee: fee.clone(),
};
let coin2 = coin(2 * amount, denom);
let msg2 = NeutronMsg::IbcTransfer {
source_port: "transfer".to_string(),
source_channel: channel,
sender: env.contract.address.to_string(),
receiver: to,
token: coin2,
timeout_height: RequestPacketTimeoutHeight {
revision_number: Some(2),
revision_height: timeout_height.or(Some(DEFAULT_TIMEOUT_HEIGHT)),
},
timeout_timestamp: 0,
memo: "".to_string(),
fee,
};
// prepare first transfer message with payload of Type1
let submsg1 = msg_with_sudo_callback(
deps.branch(),
msg1,
SudoPayload::HandlerPayload1(Type1 {
message: "message".to_string(),
}),
)?;
// prepare second transfer message with payload of Type2
// both messages have different reply ids, which allows to send them in one tx and handle both replies separately
let submsg2 = msg_with_sudo_callback(
deps.branch(),
msg2,
SudoPayload::HandlerPayload2(Type2 {
data: "data".to_string(),
}),
)?;
deps.as_ref()
.api
.debug(format!("WASMDEBUG: execute_send: sent submsg1: {:?}", submsg1).as_str());
deps.api
.debug(format!("WASMDEBUG: execute_send: sent submsg2: {:?}", submsg2).as_str());
Ok(Response::default().add_submessages(vec![submsg1, submsg2]))
}
#[entry_point]
pub fn sudo(deps: DepsMut, _env: Env, msg: TransferSudoMsg) -> StdResult<Response> {
match msg {
// For handling successful (non-error) acknowledgements
TransferSudoMsg::Response { request, data } => sudo_response(deps, request, data),
// For handling error acknowledgements
TransferSudoMsg::Error { request, details } => sudo_error(deps, request, details),
// For handling error timeouts
TransferSudoMsg::Timeout { request } => sudo_timeout(deps, request),
}
}
fn sudo_error(deps: DepsMut, req: RequestPacket, data: String) -> StdResult<Response> {
deps.api.debug(
format!(
"WASMDEBUG: sudo_error: sudo error received: {:?} {}",
req, data
)
.as_str(),
);
Ok(Response::new())
}
fn sudo_timeout(deps: DepsMut, req: RequestPacket) -> StdResult<Response> {
deps.api.debug(
format!(
"WASMDEBUG: sudo_timeout: sudo timeout ack received: {:?}",
req
)
.as_str(),
);
Ok(Response::new())
}
fn sudo_response(deps: DepsMut, req: RequestPacket, data: Binary) -> StdResult<Response> {
deps.api.debug(
format!(
"WASMDEBUG: sudo_response: sudo received: {:?} {}",
req, data
)
.as_str(),
);
let seq_id = req
.sequence
.ok_or_else(|| StdError::generic_err("sequence not found"))?;
let channel_id = req
.source_channel
.ok_or_else(|| StdError::generic_err("channel_id not found"))?;
match read_sudo_payload(deps.storage, channel_id, seq_id)? {
// here we can do different logic depending on the type of the payload we saved in msg_with_sudo_callback() call
// This allows us to distinguish different transfer message from each other.
// For example some protocols can send one transfer to refund user for some action and another transfer to top up some balance.
// Such different actions may require different handling of their responses.
SudoPayload::HandlerPayload1(t1) => sudo_callback1(deps.as_ref(), t1),
SudoPayload::HandlerPayload2(t2) => sudo_callback2(deps.as_ref(), t2),
}
// at this place we can safely remove the data under (channel_id, seq_id) key
// but it costs an extra gas, so its on you how to use the storage
}
#[entry_point]
pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult<Response> {
deps.api.debug("WASMDEBUG: migrate");
Ok(Response::default())
}
fn min_ntrn_ibc_fee(fee: IbcFee) -> IbcFee {
IbcFee {
recv_fee: fee.recv_fee,
ack_fee: fee
.ack_fee
.into_iter()
.filter(|a| a.denom == FEE_DENOM)
.collect(),
timeout_fee: fee
.timeout_fee
.into_iter()
.filter(|a| a.denom == FEE_DENOM)
.collect(),
}
}
#![warn(clippy::unwrap_used, clippy::expect_used)]
pub mod contract;
pub mod msg;
pub mod state;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
pub struct InstantiateMsg {}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
Send {
channel: String,
to: String,
denom: String,
amount: u128,
timeout_height: Option<u64>,
},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
pub struct MigrateMsg {}
use cosmwasm_std::{from_binary, to_vec, Binary, StdResult, Storage};
use cw_storage_plus::{Item, Map};
use crate::contract::SudoPayload;
pub const IBC_SUDO_ID_RANGE_START: u64 = 1_000_000_000;
pub const IBC_SUDO_ID_RANGE_SIZE: u64 = 1_000;
pub const IBC_SUDO_ID_RANGE_END: u64 = IBC_SUDO_ID_RANGE_START + IBC_SUDO_ID_RANGE_SIZE;
pub const REPLY_QUEUE_ID: Map<u64, Vec<u8>> = Map::new("reply_queue_id");
const REPLY_ID: Item<u64> = Item::new("reply_id");
/// get_next_id gives us an id for a reply msg
/// dynamic reply id helps us to pass sudo payload to sudo handler via reply handler
/// by setting unique(in transaction lifetime) id to the reply and mapping our payload to the id
/// execute ->(unique reply.id) reply (channel_id,seq_id)-> sudo handler
/// Since id uniqueless id only matters inside a transaction,
/// we can safely reuse the same id set in every new transaction
pub fn get_next_id(store: &mut dyn Storage) -> StdResult<u64> {
let mut id = REPLY_ID.may_load(store)?.unwrap_or(IBC_SUDO_ID_RANGE_START);
if id > IBC_SUDO_ID_RANGE_END {
id = IBC_SUDO_ID_RANGE_START
}
REPLY_ID.save(store, &(id + 1))?;
Ok(id)
}
pub fn save_reply_payload(store: &mut dyn Storage, payload: SudoPayload) -> StdResult<u64> {
let id = get_next_id(store)?;
REPLY_QUEUE_ID.save(store, id, &to_vec(&payload)?)?;
Ok(id)
}
pub fn read_reply_payload(store: &mut dyn Storage, id: u64) -> StdResult<SudoPayload> {
let data = REPLY_QUEUE_ID.load(store, id)?;
from_binary(&Binary(data))
}
/// SUDO_PAYLOAD - tmp storage for sudo handler payloads
/// key (String, u64) - (channel_id, seq_id)
/// every ibc chanel have its own sequence counter(autoincrement)
/// we can catch the counter in the reply msg for outgoing sudo msg
/// and save our payload for the msg
pub const SUDO_PAYLOAD: Map<(String, u64), Vec<u8>> = Map::new("sudo_payload");
pub fn save_sudo_payload(
store: &mut dyn Storage,
channel_id: String,
seq_id: u64,
payload: SudoPayload,
) -> StdResult<()> {
SUDO_PAYLOAD.save(store, (channel_id, seq_id), &to_vec(&payload)?)
}
pub fn read_sudo_payload(
store: &mut dyn Storage,
channel_id: String,
seq_id: u64,
) -> StdResult<SudoPayload> {
let data = SUDO_PAYLOAD.load(store, (channel_id, seq_id))?;
from_binary(&Binary(data))
}
// this line is added to create a gist. Empty file is not allowed.
// this line is added to create a gist. Empty file is not allowed.
// This script can be used to deploy the "Storage" contract using ethers.js library.
// Please make sure to compile "./contracts/1_Storage.sol" file before running this script.
// And use Right click -> "Run" from context menu of the file to run the script. Shortcut: Ctrl+Shift+S
import { deploy } from './ethers-lib'
(async () => {
try {
const result = await deploy('Storage', [])
console.log(`address: ${result.address}`)
} catch (e) {
console.log(e.message)
}
})()
// This script can be used to deploy the "Storage" contract using Web3 library.
// Please make sure to compile "./contracts/1_Storage.sol" file before running this script.
// And use Right click -> "Run" from context menu of the file to run the script. Shortcut: Ctrl+Shift+S
import { deploy } from './web3-lib'
(async () => {
try {
const result = await deploy('Storage', [])
console.log(`address: ${result.address}`)
} catch (e) {
console.log(e.message)
}
})()
import { ethers } from 'ethers'
/**
* Deploy the given contract
* @param {string} contractName name of the contract to deploy
* @param {Array<any>} args list of constructor' parameters
* @param {Number} accountIndex account index from the exposed account
* @return {Contract} deployed contract
*/
export const deploy = async (contractName: string, args: Array<any>, accountIndex?: number): Promise<ethers.Contract> => {
console.log(`deploying ${contractName}`)
// Note that the script needs the ABI which is generated from the compilation artifact.
// Make sure contract is compiled and artifacts are generated
const artifactsPath = `browser/contracts/artifacts/${contractName}.json` // Change this for different path
const metadata = JSON.parse(await remix.call('fileManager', 'getFile', artifactsPath))
// 'web3Provider' is a remix global variable object
const signer = (new ethers.providers.Web3Provider(web3Provider)).getSigner(accountIndex)
const factory = new ethers.ContractFactory(metadata.abi, metadata.data.bytecode.object, signer)
const contract = await factory.deploy(...args)
// The contract is NOT deployed yet; we must wait until it is mined
await contract.deployed()
return contract
}
import Web3 from 'web3'
import { Contract, ContractSendMethod, Options } from 'web3-eth-contract'
/**
* Deploy the given contract
* @param {string} contractName name of the contract to deploy
* @param {Array<any>} args list of constructor' parameters
* @param {string} from account used to send the transaction
* @param {number} gas gas limit
* @return {Options} deployed contract
*/
export const deploy = async (contractName: string, args: Array<any>, from?: string, gas?: number): Promise<Options> => {
const web3 = new Web3(web3Provider)
console.log(`deploying ${contractName}`)
// Note that the script needs the ABI which is generated from the compilation artifact.
// Make sure contract is compiled and artifacts are generated
const artifactsPath = `browser/contracts/artifacts/${contractName}.json`
const metadata = JSON.parse(await remix.call('fileManager', 'getFile', artifactsPath))
const accounts = await web3.eth.getAccounts()
const contract: Contract = new web3.eth.Contract(metadata.abi)
const contractSend: ContractSendMethod = contract.deploy({
data: metadata.data.bytecode.object,
arguments: args
})
const newContractInstance = await contractSend.send({
from: from || accounts[0],
gas: gas || 1500000
})
return newContractInstance.options
}
[package]
name = "Basics"
version = "0.0.1"
[dependencies]
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir="crates/sui-framework/packages/sui-framework/", rev = "testnet" }
[addresses]
basics = "0x0"

Basics

Very basic examples to illustrate the nuts and bolts of programming in Sui.

  • Object: a heavily commented example of a custom object.
  • Sandwich: example of object exchange logic--combining ham and bread objects to produce a sandwich.
  • Lock: example of a shared object which can be accessed if someone has a key.
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
/// This example demonstrates reading a clock object.
/// Current time is emitted as an event in the get_time transaction
module basics::clock {
use sui::clock::{Self, Clock};
use sui::event;
use sui::tx_context::TxContext;
struct TimeEvent has copy, drop {
timestamp_ms: u64,
}
/// Emit event with current time.
public entry fun get_time(clock: &Clock, _ctx: &mut TxContext) {
event::emit(TimeEvent { timestamp_ms: clock::timestamp_ms(clock) });
}
}
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
/// This example demonstrates a basic use of a shared object.
/// Rules:
/// - anyone can create and share a counter
/// - everyone can increment a counter by 1
/// - the owner of the counter can reset it to any value
module basics::counter {
use sui::transfer;
use sui::object::{Self, UID};
use sui::tx_context::{Self, TxContext};
/// A shared counter.
struct Counter has key {
id: UID,
owner: address,
value: u64
}
public fun owner(counter: &Counter): address {
counter.owner
}
public fun value(counter: &Counter): u64 {
counter.value
}
/// Create and share a Counter object.
public entry fun create(ctx: &mut TxContext) {
transfer::share_object(Counter {
id: object::new(ctx),
owner: tx_context::sender(ctx),
value: 0
})
}
/// Increment a counter by 1.
public entry fun increment(counter: &mut Counter) {
counter.value = counter.value + 1;
}
/// Set value (only runnable by the Counter owner)
public entry fun set_value(counter: &mut Counter, value: u64, ctx: &TxContext) {
assert!(counter.owner == tx_context::sender(ctx), 0);
counter.value = value;
}
/// Assert a value for the counter.
public entry fun assert_value(counter: &Counter, value: u64) {
assert!(counter.value == value, 0)
}
}
#[test_only]
module basics::counter_test {
use sui::test_scenario;
use basics::counter;
#[test]
fun test_counter() {
let owner = @0xC0FFEE;
let user1 = @0xA1;
let scenario_val = test_scenario::begin(user1);
let scenario = &mut scenario_val;
test_scenario::next_tx(scenario, owner);
{
counter::create(test_scenario::ctx(scenario));
};
test_scenario::next_tx(scenario, user1);
{
let counter_val = test_scenario::take_shared<counter::Counter>(scenario);
let counter = &mut counter_val;
assert!(counter::owner(counter) == owner, 0);
assert!(counter::value(counter) == 0, 1);
counter::increment(counter);
counter::increment(counter);
counter::increment(counter);
test_scenario::return_shared(counter_val);
};
test_scenario::next_tx(scenario, owner);
{
let counter_val = test_scenario::take_shared<counter::Counter>(scenario);
let counter = &mut counter_val;
assert!(counter::owner(counter) == owner, 0);
assert!(counter::value(counter) == 3, 1);
counter::set_value(counter, 100, test_scenario::ctx(scenario));
test_scenario::return_shared(counter_val);
};
test_scenario::next_tx(scenario, user1);
{
let counter_val = test_scenario::take_shared<counter::Counter>(scenario);
let counter = &mut counter_val;
assert!(counter::owner(counter) == owner, 0);
assert!(counter::value(counter) == 100, 1);
counter::increment(counter);
assert!(counter::value(counter) == 101, 2);
test_scenario::return_shared(counter_val);
};
test_scenario::end(scenario_val);
}
}
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
/// An example of a module that uses Shared Objects and ID linking/access.
///
/// This module allows any content to be locked inside a 'virtual chest' and later
/// be accessed by putting a 'key' into the 'lock'. Lock is shared and is visible
/// and discoverable by the key owner.
module basics::lock {
use sui::object::{Self, ID, UID};
use sui::transfer;
use sui::tx_context::{Self, TxContext};
use std::option::{Self, Option};
/// Lock is empty, nothing to take.
const ELockIsEmpty: u64 = 0;
/// Key does not match the Lock.
const EKeyMismatch: u64 = 1;
/// Lock already contains something.
const ELockIsFull: u64 = 2;
/// Lock that stores any content inside it.
struct Lock<T: store + key> has key, store {
id: UID,
locked: Option<T>
}
/// A key that is created with a Lock; is transferable
/// and contains all the needed information to open the Lock.
struct Key<phantom T: store + key> has key, store {
id: UID,
for: ID,
}
/// Returns an ID of a Lock for a given Key.
public fun key_for<T: store + key>(key: &Key<T>): ID {
key.for
}
/// Lock some content inside a shared object. A Key is created and is
/// sent to the transaction sender.
public entry fun create<T: store + key>(obj: T, ctx: &mut TxContext) {
let id = object::new(ctx);
let for = object::uid_to_inner(&id);
transfer::public_share_object(Lock<T> {
id,
locked: option::some(obj),
});
transfer::public_transfer(Key<T> {
for,
id: object::new(ctx)
}, tx_context::sender(ctx));
}
/// Lock something inside a shared object using a Key. Aborts if
/// lock is not empty or if key doesn't match the lock.
public entry fun lock<T: store + key>(
obj: T,
lock: &mut Lock<T>,
key: &Key<T>,
) {
assert!(option::is_none(&lock.locked), ELockIsFull);
assert!(&key.for == object::borrow_id(lock), EKeyMismatch);
option::fill(&mut lock.locked, obj);
}
/// Unlock the Lock with a Key and access its contents.
/// Can only be called if both conditions are met:
/// - key matches the lock
/// - lock is not empty
public fun unlock<T: store + key>(
lock: &mut Lock<T>,
key: &Key<T>,
): T {
assert!(option::is_some(&lock.locked), ELockIsEmpty);
assert!(&key.for == object::borrow_id(lock), EKeyMismatch);
option::extract(&mut lock.locked)
}
/// Unlock the Lock and transfer its contents to the transaction sender.
public fun take<T: store + key>(
lock: &mut Lock<T>,
key: &Key<T>,
ctx: &mut TxContext,
) {
transfer::public_transfer(unlock(lock, key), tx_context::sender(ctx))
}
}
#[test_only]
module basics::lockTest {
use sui::object::{Self, UID};
use sui::test_scenario;
use sui::transfer;
use basics::lock::{Self, Lock, Key};
/// Custom structure which we will store inside a Lock.
struct Treasure has store, key {
id: UID
}
#[test]
fun test_lock() {
let user1 = @0x1;
let user2 = @0x2;
let scenario_val = test_scenario::begin(user1);
let scenario = &mut scenario_val;
// User1 creates a lock and places his treasure inside.
test_scenario::next_tx(scenario, user1);
{
let ctx = test_scenario::ctx(scenario);
let id = object::new(ctx);
lock::create(Treasure { id }, ctx);
};
// Now User1 owns a key from the lock. He decides to send this
// key to User2, so that he can have access to the stored treasure.
test_scenario::next_tx(scenario, user1);
{
let key = test_scenario::take_from_sender<Key<Treasure>>(scenario);
transfer::public_transfer(key, user2);
};
// User2 is impatient and he decides to take the treasure.
test_scenario::next_tx(scenario, user2);
{
let lock_val = test_scenario::take_shared<Lock<Treasure>>(scenario);
let lock = &mut lock_val;
let key = test_scenario::take_from_sender<Key<Treasure>>(scenario);
let ctx = test_scenario::ctx(scenario);
lock::take<Treasure>(lock, &key, ctx);
test_scenario::return_shared(lock_val);
test_scenario::return_to_sender(scenario, key);
};
test_scenario::end(scenario_val);
}
}
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
/// An example of a custom object with comments explaining the relevant bits
module basics::object {
use sui::object::{Self, UID};
use sui::transfer;
use sui::tx_context::{Self, TxContext};
/// A custom sui object. Every object must have the `key` attribute
/// (indicating that it is allowed to be a key in the sui global object
/// pool), and must have a field `id: UID` corresponding to its sui ObjId.
/// Other object attributes present at the protocol level (authenticator,
/// sequence number, TxDigest, ...) are intentionally not exposed here.
struct Object has key {
id: UID,
/// Custom objects can have fields of arbitrary type...
custom_field: u64,
/// ... including other objects
child_obj: ChildObject,
/// ... and other global objects
nested_obj: AnotherObject,
}
/// An object that can be stored inside global objects or other child
/// objects, but cannot be placed in the global object pool on its own.
/// Note that it doesn't need an ID field.
struct ChildObject has store {
a_field: bool,
}
/// An object that can live either in the global object pool or as a nested
/// object.
struct AnotherObject has key, store {
id: UID,
}
/// Example of updating an object. All Move fields are private, so the
/// fields of `Object` can only be (directly) updated by code in this
/// module.
public fun write_field(o: &mut Object, v: u64) {
if (some_conditional_logic()) {
o.custom_field = v
}
}
/// Example of transferring an object to a a new owner. A struct can only
/// be transferred by the module that declares it.
public fun transfer(o: Object, recipient: address) {
assert!(some_conditional_logic(), 0);
transfer::transfer(o, recipient)
}
/// Simple getter
public fun read_field(o: &Object): u64 {
o.custom_field
}
/// Example of creating a object by deriving a unique ID from the current
/// transaction and returning it to the caller (who may call functions
/// from this module to read/write it, package it into another object, ...)
public fun create(tx: &mut TxContext): Object {
Object {
id: object::new(tx),
custom_field: 0,
child_obj: ChildObject { a_field: false },
nested_obj: AnotherObject { id: object::new(tx) }
}
}
/// Example of an entrypoint function to be embedded in a Sui
/// transaction. A possible argument of an entrypoint function is a
/// `TxContext` created by the runtime that is useful for deriving
/// new id's or determining the sender of the transaction.
/// Next to the `TxContext`, entrypoints can take struct types with the `key`
/// attribute as input, as well as primitive types like ints, bools, ...
///
/// A Sui transaction must declare the ID's of each object it will
/// access + any primitive inputs. The runtime that processes the
/// transaction fetches the values associated with the ID's, type-checks
/// the values + primitive inputs against the function signature
/// , then calls the `main` function with these values.
///
/// If the script terminates successfully, the runtime collects changes to
/// input objects + created objects + emitted events, increments the
/// sequence number of each object, creates a hash that commits to the
/// outputs, etc.
public entry fun main(
to_read: &Object, // The argument of type Object is passed as a read-only reference
to_write: &mut Object, // The argument is passed as a mutable reference
to_consume: Object, // The argument is passed as a value
// ... end objects, begin primitive type inputs
int_input: u64,
recipient: address,
ctx: &mut TxContext,
) {
let v = read_field(to_read);
write_field(to_write, v + int_input);
transfer(to_consume, recipient);
// demonstrate creating a new object for the sender
let sender = tx_context::sender(ctx);
transfer::transfer(create(ctx), sender)
}
fun some_conditional_logic(): bool {
// placeholder for checks implemented in arbitrary Move code
true
}
}
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
/// Test CTURD object basics (create, transfer, update, read, delete)
module basics::object_basics {
use sui::event;
use sui::object::{Self, UID};
use sui::tx_context::{Self, TxContext};
use sui::transfer;
struct Object has key, store {
id: UID,
value: u64,
}
struct Wrapper has key {
id: UID,
o: Object
}
struct NewValueEvent has copy, drop {
new_value: u64
}
public entry fun create(value: u64, recipient: address, ctx: &mut TxContext) {
transfer::public_transfer(
Object { id: object::new(ctx), value },
recipient
)
}
public entry fun transfer(o: Object, recipient: address) {
transfer::public_transfer(o, recipient)
}
public entry fun freeze_object(o: Object) {
transfer::public_freeze_object(o)
}
public entry fun set_value(o: &mut Object, value: u64) {
o.value = value;
}
// test that reading o2 and updating o1 works
public entry fun update(o1: &mut Object, o2: &Object) {
o1.value = o2.value;
// emit an event so the world can see the new value
event::emit(NewValueEvent { new_value: o2.value })
}
public entry fun delete(o: Object) {
let Object { id, value: _ } = o;
object::delete(id);
}
public entry fun wrap(o: Object, ctx: &mut TxContext) {
transfer::transfer(Wrapper { id: object::new(ctx), o }, tx_context::sender(ctx))
}
public entry fun unwrap(w: Wrapper, ctx: &mut TxContext) {
let Wrapper { id, o } = w;
object::delete(id);
transfer::public_transfer(o, tx_context::sender(ctx))
}
}
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
/// Example of objects that can be combined to create
/// new objects
module basics::sandwich {
use sui::balance::{Self, Balance};
use sui::coin::{Self, Coin};
use sui::object::{Self, UID};
use sui::sui::SUI;
use sui::transfer;
use sui::tx_context::{Self, TxContext};
struct Ham has key {
id: UID
}
struct Bread has key {
id: UID
}
struct Sandwich has key {
id: UID,
}
// This Capability allows the owner to withdraw profits
struct GroceryOwnerCapability has key {
id: UID
}
// Grocery is created on module init
struct Grocery has key {
id: UID,
profits: Balance<SUI>
}
/// Price for ham
const HAM_PRICE: u64 = 10;
/// Price for bread
const BREAD_PRICE: u64 = 2;
/// Not enough funds to pay for the good in question
const EInsufficientFunds: u64 = 0;
/// Nothing to withdraw
const ENoProfits: u64 = 1;
/// On module init, create a grocery
fun init(ctx: &mut TxContext) {
transfer::share_object(Grocery {
id: object::new(ctx),
profits: balance::zero<SUI>()
});
transfer::transfer(GroceryOwnerCapability {
id: object::new(ctx)
}, tx_context::sender(ctx));
}
/// Exchange `c` for some ham
public entry fun buy_ham(
grocery: &mut Grocery,
c: Coin<SUI>,
ctx: &mut TxContext
) {
let b = coin::into_balance(c);
assert!(balance::value(&b) == HAM_PRICE, EInsufficientFunds);
balance::join(&mut grocery.profits, b);
transfer::transfer(Ham { id: object::new(ctx) }, tx_context::sender(ctx))
}
/// Exchange `c` for some bread
public entry fun buy_bread(
grocery: &mut Grocery,
c: Coin<SUI>,
ctx: &mut TxContext
) {
let b = coin::into_balance(c);
assert!(balance::value(&b) == BREAD_PRICE, EInsufficientFunds);
balance::join(&mut grocery.profits, b);
transfer::transfer(Bread { id: object::new(ctx) }, tx_context::sender(ctx))
}
/// Combine the `ham` and `bread` into a delicious sandwich
public entry fun make_sandwich(
ham: Ham, bread: Bread, ctx: &mut TxContext
) {
let Ham { id: ham_id } = ham;
let Bread { id: bread_id } = bread;
object::delete(ham_id);
object::delete(bread_id);
transfer::transfer(Sandwich { id: object::new(ctx) }, tx_context::sender(ctx))
}
/// See the profits of a grocery
public fun profits(grocery: &Grocery): u64 {
balance::value(&grocery.profits)
}
/// Owner of the grocery can collect profits by passing his capability
public entry fun collect_profits(_cap: &GroceryOwnerCapability, grocery: &mut Grocery, ctx: &mut TxContext) {
let amount = balance::value(&grocery.profits);
assert!(amount > 0, ENoProfits);
// Take a transferable `Coin` from a `Balance`
let coin = coin::take(&mut grocery.profits, amount, ctx);
transfer::public_transfer(coin, tx_context::sender(ctx));
}
#[test_only]
public fun init_for_testing(ctx: &mut TxContext) {
init(ctx);
}
}
#[test_only]
module basics::test_sandwich {
use basics::sandwich::{Self, Grocery, GroceryOwnerCapability, Bread, Ham};
use sui::test_scenario;
use sui::coin::{Self};
use sui::sui::SUI;
#[test]
fun test_make_sandwich() {
let owner = @0x1;
let the_guy = @0x2;
let scenario_val = test_scenario::begin(owner);
let scenario = &mut scenario_val;
test_scenario::next_tx(scenario, owner);
{
sandwich::init_for_testing(test_scenario::ctx(scenario));
};
test_scenario::next_tx(scenario, the_guy);
{
let grocery_val = test_scenario::take_shared<Grocery>(scenario);
let grocery = &mut grocery_val;
let ctx = test_scenario::ctx(scenario);
sandwich::buy_ham(
grocery,
coin::mint_for_testing<SUI>(10, ctx),
ctx
);
sandwich::buy_bread(
grocery,
coin::mint_for_testing<SUI>(2, ctx),
ctx
);
test_scenario::return_shared( grocery_val);
};
test_scenario::next_tx(scenario, the_guy);
{
let ham = test_scenario::take_from_sender<Ham>(scenario);
let bread = test_scenario::take_from_sender<Bread>(scenario);
sandwich::make_sandwich(ham, bread, test_scenario::ctx(scenario));
};
test_scenario::next_tx(scenario, owner);
{
let grocery_val = test_scenario::take_shared<Grocery>(scenario);
let grocery = &mut grocery_val;
let capability = test_scenario::take_from_sender<GroceryOwnerCapability>(scenario);
assert!(sandwich::profits(grocery) == 12, 0);
sandwich::collect_profits(&capability, grocery, test_scenario::ctx(scenario));
assert!(sandwich::profits(grocery) == 0, 0);
test_scenario::return_to_sender(scenario, capability);
test_scenario::return_shared(grocery_val);
};
test_scenario::end(scenario_val);
}
}
[package]
name = "Bridge"
version = "0.0.1"
[dependencies]
#MoveStdlib = { local = "../../../crates/sui-framework/packages/move-stdlib" }
#Sui = { local = "../../../crates/sui-framework/packages/sui-framework" }
MoveStdlib = { git = "https://github.com/MystenLabs/sui.git", subdir="crates/sui-framework/packages/move-stdlib", rev = "testnet" }
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir="crates/sui-framework/packages/sui-framework/", rev = "testnet" }
[addresses]
bridge = "0x0"
oRzrCwYAAAALAQAiAiJsA44B3QIE6wNCBa0EzQMH+geUCgiOEmAG7hJ+CuwTYgzOFI4IDdwcFgAcACEAJwBFAEgAZQFOAhgCGwImAjICQAJNAmQCZgJoAmwAAQgAAAMEAAAMAwAABgYAABADAAARAwAADgMAAA8DAAICBAADBAcAAwUHAAMJAgADDQIABQcEAAYLBwEAAAgABAEAAQkIDAEAAQkSDAEAAQsKDAIHAAQBDBQEAA4TAgAPFQcCAQAAABAWDAAAOQABAABCAgMAAEEEBQAAFwYBAQAAUgcBAQAAGggBAAAkCQoBAAAjCQsBAAAiCQEBAAAzCAEAAEsMDQABPiUmAAFaARAAAikTFAACajoBAAMqPS8AAysrLAADL0gQAAM0LkcAAzUuPgADPy4vAANHLhAAA1UuDQADWS4QAANfPw0AA2E/KgADYj8QAANjPxAABC4BEAAEXgEQAAUXIwEBAAUfLQEBAAUpAA8ABUlCQQEABWABEAEABiwxAQEABi0xHAEABjw3JgEABj03JgEABkwBMQEABlgcMQEABzYqKQAHXSkqAAhnKA0BAAkbJygBAAowHAEBAwsoOSYCBwQLSgAWAgcEC1AyAQIHBAtRNTYCBwQMSgAXAA1PRQEBDA1XHAEBCA5TEykADyhJJgIBAA8xARICAQAPO0oBAgEAD1FLTAIBABApGRoBBBBDHiIBBBBEHyABBBBrHg0ANxEvFToYNBs8GDsYHhwiHCwcKxwfHCcwMBUtMzEVJjAtOC4VKDAtOy1AJ0EhHC1DKEEGHCVBI0EkQTNBNhE4ETkRAQcIFAABBwgAAQcIAQEGCAABBggBAgcIAAsRAQkABQcIAAIKAgsQAQkABwgUAwcIAAgJCgoCBAcIAAIDBwgUAgsOAQsQAQkABQELDgELEAEJAAIHCAECAQMCCAEIDQEIDQECAgIDAQsVAgkACQEBBggUAQgIAggKCAMBCxICCQAJAQEIEwEIAQMDCQAHCBQBCBYBCAABCQACBwgBAwEGCBYBBwgWAQcJAAIGCAEDAQYJAAIHCA0LEQEJAAYDBwgBCAoICQMCAgICAQEBBgsQAQkAAQYLDwEJAAEFAQoCBwIDCgICCgICAwEICQMHCA0LEAEJAAYIFAEGCAkBCAoBCgoCAQsOAQkAAwcLEgIJAAkBCQAJAQEIAgQBBwgBCAoIAwIHCxICCQAJAQkAAQkBAQYLDgEJAAEIBgIGCxICCQAJAQkAAwYICAgJCgoCAQgECQEHCAEICggJBQsOAQoKAgILEAEJAAgMAwICAwEIDAEGCAwBCAcBCxABCQADBwgNAwcIFAEIBQIFCw4BCxABCQACCQAFAwMHCAEICwEICwEGCAsCBgsVAgkACQEGCQADBwsVAgkACQEJAAkBAgcLFQIJAAkBBgkAAgkACQEHQmFsYW5jZQZCcmlkZ2UPQnJpZGdlQ29tbWl0dGVlC0JyaWRnZUlubmVyDUJyaWRnZU1lc3NhZ2UQQnJpZGdlTWVzc2FnZUtleQxCcmlkZ2VSZWNvcmQOQnJpZGdlVHJlYXN1cnkEQ29pbgtFbWVyZ2VuY3lPcAtMaW5rZWRUYWJsZQZPcHRpb24QVG9rZW5CcmlkZ2VFdmVudAxUb2tlblBheWxvYWQcVG9rZW5UcmFuc2ZlckFscmVhZHlBcHByb3ZlZBtUb2tlblRyYW5zZmVyQWxyZWFkeUNsYWltZWQVVG9rZW5UcmFuc2ZlckFwcHJvdmVkFFRva2VuVHJhbnNmZXJDbGFpbWVkC1RyZWFzdXJ5Q2FwCVR4Q29udGV4dANVSUQGVmVjTWFwCVZlcnNpb25lZBBhZGRfdHJlYXN1cnlfY2FwB2FkZHJlc3MGYW1vdW50FmFwcHJvdmVfYnJpZGdlX21lc3NhZ2UHYmFsYW5jZQZicmlkZ2UOYnJpZGdlX3JlY29yZHMOYnJpZGdlX3ZlcnNpb24EYnVybghjaGFpbl9pZAljaGFpbl9pZHMYY2xhaW1fYW5kX3RyYW5zZmVyX3Rva2VuC2NsYWltX3Rva2VuFGNsYWltX3Rva2VuX2ludGVybmFsB2NsYWltZWQEY29pbgljb21taXR0ZWUIY29udGFpbnMGY3JlYXRlCmNyZWF0ZV9rZXkbY3JlYXRlX3Rva2VuX2JyaWRnZV9tZXNzYWdlDGRlc3Ryb3lfbm9uZQxkZXN0cm95X3NvbWUMZW1lcmdlbmN5X29wEWVtZXJnZW5jeV9vcF90eXBlBGVtaXQFZW1wdHkFZXZlbnQUZXhlY3V0ZV9lbWVyZ2VuY3lfb3AcZXh0cmFjdF9lbWVyZ2VuY3lfb3BfcGF5bG9hZBxleHRyYWN0X3Rva2VuX2JyaWRnZV9wYXlsb2FkCmZyb21fYnl0ZXMGZnJvemVuAmlkBGluaXQFaW5uZXIGaW5zZXJ0B2lzX25vbmUHaXNfc29tZQ5pc192YWxpZF9yb3V0ZQNrZXkMbGlua2VkX3RhYmxlCmxvYWRfaW5uZXIObG9hZF9pbm5lcl9tdXQKbG9hZF92YWx1ZQ5sb2FkX3ZhbHVlX211dAdtZXNzYWdlC21lc3NhZ2Vfa2V5DG1lc3NhZ2VfdHlwZQ1tZXNzYWdlX3R5cGVzBG1pbnQDbmV3DG5leHRfc2VxX251bQRub25lBm9iamVjdAZvcHRpb24PcHVibGljX3RyYW5zZmVyCXB1c2hfYmFjawZyZW1vdmUKc2VuZF90b2tlbgZzZW5kZXIOc2VuZGVyX2FkZHJlc3MHc2VxX251bQ1zZXF1ZW5jZV9udW1zDHNoYXJlX29iamVjdARzb21lDHNvdXJjZV9jaGFpbg5zdWlfbG9jYWxfdGVzdA50YXJnZXRfYWRkcmVzcwx0YXJnZXRfY2hhaW4IdG9fYnl0ZXMFdG9rZW4MdG9rZW5fYW1vdW50CHRva2VuX2lkFHRva2VuX3RhcmdldF9hZGRyZXNzEnRva2VuX3RhcmdldF9jaGFpbgp0b2tlbl90eXBlCHRyYW5zZmVyCHRyZWFzdXJ5CnR4X2NvbnRleHQFdmFsdWUHdmVjX21hcBN2ZXJpZmllZF9zaWduYXR1cmVzEXZlcmlmeV9zaWduYXR1cmVzB3ZlcnNpb24JdmVyc2lvbmVkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgEAAgEBAwgAAAAAAAAAAAMIAQAAAAAAAAADCAIAAAAAAAAAAwgDAAAAAAAAAAMIBAAAAAAAAAADCAYAAAAAAAAAAwgHAAAAAAAAAAMICgAAAAAAAAADCAsAAAAAAAAAAwgMAAAAAAAAAAMIDQAAAAAAAAADCA4AAAAAAAAAAAICOAgTOggWAQIHHgMgAlYLFQICAycICGUIDR0LEgIICggDNwECAghHAlUDWQJUCgJcAlsKAmMCGQMDAgNFCAlpCw4BCgoCJQEEAgFGCAoFAgFGCAoGAgFGCAoHAgFGCAoAAAAADhgK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
---
compiled_package_info:
package_name: Bridge
address_alias_instantiation:
bridge: "0000000000000000000000000000000000000000000000000000000000000000"
std: "0000000000000000000000000000000000000000000000000000000000000001"
sui: "0000000000000000000000000000000000000000000000000000000000000002"
source_digest: 1F2AA252D62C81D69356870F4D5E1C4A573F8BA878438448CE6B86CB21D737ED
build_flags:
dev_mode: false
test_mode: false
generate_docs: false
install_dir: ~
force_recompilation: false
lock_file: "./Move.lock"
fetch_deps_only: false
skip_fetch_latest_git_deps: false
default_flavor: sui
default_edition: ~
deps_as_root: false
silence_warnings: false
warnings_are_errors: false
json_errors: false
additional_named_addresses: {}
lint_flag:
no_lint: false
lint: false
dependencies:
- MoveStdlib
- Sui
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
{"modules":["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CSEEFzEDDAAFIAsDOAohBBwFHgcAJzEEDAALAAwBCwEMAgsCAgAAAA0A","oRzrCwYAAAAHAQACAwIPBREDBxQ1CEkgBmkJDHIbAAIAAwABAAAAAAEAAAEAAQAAAQITY29tbWl0dGVlX2Jsb2NrbGlzdAxlbWVyZ2VuY3lfb3ANbWVzc2FnZV90eXBlcwV0b2tlbgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgEAAgEBAgECAAEAAAACBwACAQEAAAACBwECAgEAAAACBwICAA==","oRzrCwYAAAALAQAIAggYAyCLAQSrAQgFswFqB50C0QQI7gZgBs4HEQrfBzAMjwj1Aw2EDBIAFQAXAS0CCAAFAgAAAQcAAAIHAAAEAgAAAwIAAwAHAAARAAEAABAAAgAAIwMEAAAMBQMAAAoGAwAACwcIAAAUAAgAABYACQAAIgAKAAAkAAkAACsLCQAAKgsEAAAsCwkAACkLCgAADwwJAAAgBAQAABwNCgABDg8JAAEoDwkAAgcVDwEAAhMREgEAAh8WDwEAAxIQBAADGQQQAAMdDQkAAx4NBAADJxQEAQAUCRoKEwkVCQEGCAEBCAMBCAQBCAEBCgIHAgMKAgIKAgIDAwIDAgMCAgMBCAIBAgEDAQYIAwEGCAQBBwgFBwoCAwgFCgIKAgICAAEIBQEGCgkAAQEGCgICAgoCAwIBBgkAAgcKCQAKCQABBwoJAAMDAgMDQkNTDUJyaWRnZU1lc3NhZ2UQQnJpZGdlTWVzc2FnZUtleQtFbWVyZ2VuY3lPcAxUb2tlblBheWxvYWQEVVNEQwZhbW91bnQGYXBwZW5kA2Jjcw5icmlkZ2Vfc2VxX251bRtjcmVhdGVfZW1lcmdlbmN5X29wX21lc3NhZ2UKY3JlYXRlX2tleRtjcmVhdGVfdG9rZW5fYnJpZGdlX21lc3NhZ2ULZHVtbXlfZmllbGQMZW1lcmdlbmN5X29wEWVtZXJnZW5jeV9vcF90eXBlHGV4dHJhY3RfZW1lcmdlbmN5X29wX3BheWxvYWQcZXh0cmFjdF90b2tlbl9icmlkZ2VfcGF5bG9hZBRpbnRvX3JlbWFpbmRlcl9ieXRlcwhpc19lbXB0eQNrZXkHbWVzc2FnZQxtZXNzYWdlX3R5cGUNbWVzc2FnZV90eXBlcw9tZXNzYWdlX3ZlcnNpb24DbmV3B29wX3R5cGUHcGF5bG9hZAtwZWVsX3U2NF9iZQdwZWVsX3U4C3BlZWxfdmVjX3U4B3JldmVyc2UNcmV2ZXJzZV9ieXRlcw5zZW5kZXJfYWRkcmVzcwdzZXFfbnVtEXNlcmlhbGl6ZV9tZXNzYWdlDHNvdXJjZV9jaGFpbg50YXJnZXRfYWRkcmVzcwx0YXJnZXRfY2hhaW4IdG9fYnl0ZXMFdG9rZW4MdG9rZW5fYW1vdW50FHRva2VuX3RhcmdldF9hZGRyZXNzEnRva2Vu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AwIHCxICCQAJAQkAAQkBAQYLDgEJAAEIBgIGCxICCQAJAQkAAwYICAgJCgoCAQgECQEHCAEICggJBQsOAQoKAgILEAEJAAgMAwICAwEIDAEGCAwBCAcBCxABCQADBwgNAwcIFAEIBQIFCw4BCxABCQACCQAFAwMHCAEICwEICwEGCAsCBgsVAgkACQEGCQADBwsVAgkACQEJAAkBAgcLFQIJAAkBBgkAAgkACQEHQmFsYW5jZQZCcmlkZ2UPQnJpZGdlQ29tbWl0dGVlC0JyaWRnZUlubmVyDUJyaWRnZU1lc3NhZ2UQQnJpZGdlTWVzc2FnZUtleQxCcmlkZ2VSZWNvcmQOQnJpZGdlVHJlYXN1cnkEQ29pbgtFbWVyZ2VuY3lPcAtMaW5rZWRUYWJsZQZPcHRpb24QVG9rZW5CcmlkZ2VFdmVudAxUb2tlblBheWxvYWQcVG9rZW5UcmFuc2ZlckFscmVhZHlBcHByb3ZlZBtUb2tlblRyYW5zZmVyQWxyZWFkeUNsYWltZWQVVG9rZW5UcmFuc2ZlckFwcHJvdmVkFFRva2VuVHJhbnNmZXJDbGFpbWVkC1RyZWFzdXJ5Q2FwCVR4Q29udGV4dANVSUQGVmVjTWFwCVZlcnNpb25lZBBhZGRfdHJlYXN1cnlfY2FwB2FkZHJlc3MGYW1vdW50FmFwcHJvdmVfYnJpZGdlX21lc3NhZ2UHYmFsYW5jZQZicmlkZ2UOYnJpZGdlX3JlY29yZHMOYnJpZGdlX3ZlcnNpb24EYnVybghjaGFpbl9pZAljaGFpbl9pZHMYY2xhaW1fYW5kX3RyYW5zZmVyX3Rva2VuC2NsYWltX3Rva2VuFGNsYWltX3Rva2VuX2ludGVybmFsB2NsYWltZWQEY29pbgljb21taXR0ZWUIY29udGFpbnMGY3JlYXRlCmNyZWF0ZV9rZXkbY3JlYXRlX3Rva2VuX2JyaWRnZV9tZXNzYWdlDGRlc3Ryb3lfbm9uZQxkZXN0cm95X3NvbWUMZW1lcmdlbmN5X29wEWVtZXJnZW5jeV9vcF90eXBlBGVtaXQFZW1wdHkFZXZlbnQUZXhlY3V0ZV9lbWVyZ2VuY3lfb3AcZXh0cmFjdF9lbWVyZ2VuY3lfb3BfcGF5bG9hZBxleHRyYWN0X3Rva2VuX2JyaWRnZV9wYXlsb2FkCmZyb21fYnl0ZXMGZnJvemVuAmlkBGluaXQFaW5uZXIGaW5zZXJ0B2lzX25vbmUHaXNfc29tZQ5pc192YWxpZF9yb3V0ZQNrZXkMbGlua2VkX3RhYmxlCmxvYWRfaW5uZXIObG9hZF9pbm5lcl9tdXQKbG9hZF92YWx1ZQ5sb2FkX3ZhbHVlX211dAdtZXNzYWdlC21lc3NhZ2Vfa2V5DG1lc3NhZ2VfdHlwZQ1tZXNzYWdlX3R5cGVzBG1pbnQDbmV3DG5leHRfc2VxX251bQRub25lBm9iamVjdAZvcHRpb24PcHVibGljX3RyYW5zZmVyCXB1c2hfYmFjawZyZW1vdmUKc2VuZF90b2tlbgZzZW5kZXIOc2VuZGVyX2FkZHJlc3MHc2VxX251bQ1zZXF1ZW5jZV9udW1zDHNoYXJlX29iamVjdARzb21lDHNvdXJjZV9jaGFpbg5zdWlfbG9jYWxfdGVzdA50YXJnZXRfYWRkcmVzcwx0YXJnZXRfY2hhaW4IdG9fYnl0ZXMFdG9rZW4MdG9rZW5fYW1vdW50CHRva2VuX2lkFHRva2VuX3RhcmdldF9hZGRyZXNzEnRva2VuX3RhcmdldF9jaGFpbgp0b2tlbl90eXBlCHRyYW5zZmVyCHRyZWFzdXJ5CnR4X2NvbnRleHQFdmFsdWUHdmVjX21hcBN2ZXJpZmllZF9zaWduYXR1cmVzEXZlcmlmeV9zaWduYXR1cmVzB3ZlcnNpb24JdmVyc2lvbmVkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgEAAgEBAwgAAAAAAAAAAAMIAQAAAAAAAAADCAIAAAAAAAAAAwgDAAAAAAAAAAMIBAAAAAAAAAADCAYAAAAAAAAAAwgHAAAAAAAAAAMICgAAAAAAAAADCAsAAAAAAAAAAwgMAAAAAAAAAAMIDQAAAAAAAAADCA4AAAAAAAAAAAICOAgTOggWAQIHHgMgAlYLFQICAycICGUIDR0LEgIICggDNwECAghHAlUDWQJUCgJcAlsKAmMCGQMDAgNFCAlpCw4BCgoCJQEEAgFGCAoFAgFGCAoGAgFGCAoHAgFGCAoAAAAADhgKABEgDAIHAxEMOAAKAC4RDQsCCgA4AQkSAQwBCgARMgcDCwELADgCEgA4AwIBAAAAHR4KABAAET0MAgoCBwMhBAkFDQsAAQcIJwsADwA4BAwBCgEQARQLAiEEGAUcCwEBBwgnCwECAgAAACEeCgAQABE9DAIKAgcDIQQJBQ0LAAEHCCcLABAAOAUMAQoBEAEUCwIhBBgFHAsBAQcIJwsBAgMBAAABBgsAEQEPAgsBOAYCBAEAACRVCwARAQwGCgYQAxQKARELBAoFEAsGAQsEAQcLJwoGEAQUIAQWBRwLBgELBAEHCScKBhEdEQoMBTgHDAoOAzgIOAkMCQoGEAMUCgUKBC4RNREqCgEKAgoKCgkREAwICgYPAgsDCgQuOAoOCBEUDAcKBg8FCwcLCDgLCRIDOAwRHQsFCwYQAxQLBC4RNREqCwELAgsKCwkSAjgNAgUBAAA0WwsAEQEMBA4BERQMBQ4BERURHSEEEw4BERcKBBADFCEMAwUVCQwDCwMEPgoEDwUKBTgODAYOBhAGFAoBIQQjBScLBAEHBCcOBhAHFCAELQUxCwQBBwwnDgYQCDgPBD4KBRIGOBALBA8FCwULBjgMAgoEEAUKBTgRBEkLBAELBRIGOBACCgQQCQoBCgIRDgsEDwUKBQsBCwI4EgkSAzgMCwUSBDgTAgYAAAA8iAELABEBDAUKAREdCwIRDwwGCgUQBQoGOBEgBBQLBQELAwEHDScKBQ8FCgY4DhMDDAQMCQwHDgcRFREdIQQiBSgLBQELAwEHAicOCTgPBCwFMgsFAQsDAQcDJw4HERMMDA4MERkRKQwICwQESwsDAQoGEgc4FAsFDwULBgsHCwkIEgM4DDgVCwgCDgwRGgwKCgoKBRADFCEEVQVbCwUBCwMBBwYnCwELChELBGAFZgsFAQsDAQcLJzgHDgwRGyEEbAVyCwUBCwMBBwUnCgUPAg4MERgLAzgWDAsLBQ8FCgYLBwsJCBIDOAwLBhIFOBcLCzgYCwgCBwEAAEQSCwALAQsCCgM4GQwEDAULAy4RNQsEIQQOBRAHAycLBQIIAQAARBILAAsBCwILAzgZDAQMBQ4FOBoEDQsFOBsCCwU4HAsEOB0CCQEAAEZADgERFREcIQQGBQoLAAEHAicLABEBDAQKBBEcEQoMAw4BERYLAyEEFwUbCwQBBwcnCgQQCQoBCwIRDg4BERIMBQ4FEREHACEELwsEEAQUCCEBBT8OBRERBwEhBDUFOQsEAQcKJwsEEAQUCSEBAgoAAAARHAoAEAoOATgeIAQNCwAPCgsBBgEAAAAAAAAAOB8GAAAAAAAAAAACCgAPCg4BOCAMAwwCCwAPCgsCCgMGAQAAAAAAAAAWOB8LAwIAAQEAAQQBAQEGAQUDAAMCAwEBAwECAA=="],"dependencies":["0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000002"],"digest":[208,213,163,25,212,83,196,244,106,1,111,250,0,189,194,148,15,127,49,37,60,70,135,100,82,70,174,173,244,66,177,63]}
oRzrCwYAAAALAQAMAgweAyoiBEwIBVRUB6gBtQEI3QJgBr0DLQrqAwUM7wMoD5cEAgAJAQwCBgIQAhICEwABAgABAgcBAAACAAwBAAECAwwBAAEEBAIABQUHAAAKAAEAAQsBBAEAAgcGBwECAw0JAQEMAw4NAQEMBA8KCwABAwIFAwgEDAIIAAcIBAACCwIBCAALAwEIAAEIBQELAQEJAAEIAAcJAAIKAgoCCgILAQEIBQcIBAILAwEJAAsCAQkAAQsCAQgAAQkAAQYIBAEFAQsDAQgAAgkABQxDb2luTWV0YWRhdGEDRVRIBk9wdGlvbgtUcmVhc3VyeUNhcAlUeENvbnRleHQDVXJsBGNvaW4PY3JlYXRlX2N1cnJlbmN5C2R1bW15X2ZpZWxkA2V0aARpbml0BG5vbmUGb3B0aW9uFHB1YmxpY19mcmVlemVfb2JqZWN0D3B1YmxpY190cmFuc2ZlcgZzZW5kZXIIdHJhbnNmZXIIdHJlYXN1cnkKdHhfY29udGV4dAN1cmwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIKAgQDRVRICgIJCEV0aGVyZXVtCgIXFkJyaWRnZWQgRXRoZXJldW0gdG9rZW4AAgEIAQAAAAACEgsAMQgHAAcBBwI4AAoBOAEMAgwDCwI4AgsDCwEuEQU4AwIAEQA=
oRzrCwYAAAALAQAIAggYAyCLAQSrAQgFswFqB50C0QQI7gZgBs4HEQrfBzAMjwj1Aw2EDBIAFQAXAS0CCAAFAgAAAQcAAAIHAAAEAgAAAwIAAwAHAAARAAEAABAAAgAAIwMEAAAMBQMAAAoGAwAACwcIAAAUAAgAABYACQAAIgAKAAAkAAkAACsLCQAAKgsEAAAsCwkAACkLCgAADwwJAAAgBAQAABwNCgABDg8JAAEoDwkAAgcVDwEAAhMREgEAAh8WDwEAAxIQBAADGQQQAAMdDQkAAx4NBAADJxQEAQAUCRoKEwkVCQEGCAEBCAMBCAQBCAEBCgIHAgMKAgIKAgIDAwIDAgMCAgMBCAIBAgEDAQYIAwEGCAQBBwgFBwoCAwgFCgIKAgICAAEIBQEGCgkAAQEGCgICAgoCAwIBBgkAAgcKCQAKCQABBwoJAAMDAgMDQkNTDUJyaWRnZU1lc3NhZ2UQQnJpZGdlTWVzc2FnZUtleQtFbWVyZ2VuY3lPcAxUb2tlblBheWxvYWQEVVNEQwZhbW91bnQGYXBwZW5kA2Jjcw5icmlkZ2Vfc2VxX251bRtjcmVhdGVfZW1lcmdlbmN5X29wX21lc3NhZ2UKY3JlYXRlX2tleRtjcmVhdGVfdG9rZW5fYnJpZGdlX21lc3NhZ2ULZHVtbXlfZmllbGQMZW1lcmdlbmN5X29wEWVtZXJnZW5jeV9vcF90eXBlHGV4dHJhY3RfZW1lcmdlbmN5X29wX3BheWxvYWQcZXh0cmFjdF90b2tlbl9icmlkZ2VfcGF5bG9hZBRpbnRvX3JlbWFpbmRlcl9ieXRlcwhpc19lbXB0eQNrZXkHbWVzc2FnZQxtZXNzYWdlX3R5cGUNbWVzc2FnZV90eXBlcw9tZXNzYWdlX3ZlcnNpb24DbmV3B29wX3R5cGUHcGF5bG9hZAtwZWVsX3U2NF9iZQdwZWVsX3U4C3BlZWxfdmVjX3U4B3JldmVyc2UNcmV2ZXJzZV9ieXRlcw5zZW5kZXJfYWRkcmVzcwdzZXFfbnVtEXNlcmlhbGl6ZV9tZXNzYWdlDHNvdXJjZV9jaGFpbg50YXJnZXRfYWRkcmVzcwx0YXJnZXRfY2hhaW4IdG9fYnl0ZXMFdG9rZW4MdG9rZW5fYW1vdW50FHRva2VuX3RhcmdldF9hZGRyZXNzEnRva2Vu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
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
module bridge::bridge {
use std::option;
use std::option::{none, Option, some};
use sui::object::{Self, UID};
use sui::address;
use sui::balance;
use sui::coin::{Self, Coin};
use sui::coin::TreasuryCap;
use sui::event::emit;
use sui::linked_table::{Self, LinkedTable};
use sui::transfer;
use sui::tx_context::{Self, TxContext};
use sui::vec_map::{Self, VecMap};
use sui::versioned::{Self, Versioned};
use bridge::chain_ids::{Self, sui_local_test};
use bridge::committee::{Self, BridgeCommittee};
use bridge::message::{Self, BridgeMessage, BridgeMessageKey};
use bridge::message_types;
use bridge::treasury::{Self, BridgeTreasury};
struct Bridge has key {
id: UID,
inner: Versioned
}
struct BridgeInner has store {
bridge_version: u64,
chain_id: u8,
// nonce for replay protection
sequence_nums: VecMap<u8, u64>,
// committee
committee: BridgeCommittee,
// Bridge treasury for mint/burn bridged tokens
treasury: BridgeTreasury,
bridge_records: LinkedTable<BridgeMessageKey, BridgeRecord>,
frozen: bool,
}
// Emergency Op types
const FREEZE: u8 = 0;
const UNFREEZE: u8 = 1;
struct TokenBridgeEvent has copy, drop {
message_type: u8,
seq_num: u64,
source_chain: u8,
sender_address: vector<u8>,
target_chain: u8,
target_address: vector<u8>,
token_type: u8,
amount: u64
}
struct BridgeRecord has store, drop {
message: BridgeMessage,
verified_signatures: Option<vector<vector<u8>>>,
claimed: bool
}
const EUnexpectedMessageType: u64 = 0;
const EUnauthorisedClaim: u64 = 1;
const EMalformedMessageError: u64 = 2;
const EUnexpectedTokenType: u64 = 3;
const EUnexpectedChainID: u64 = 4;
// const ENotSystemAddress: u64 = 5;
const EUnexpectedSeqNum: u64 = 6;
const EWrongInnerVersion: u64 = 7;
const EBridgeUnavailable: u64 = 10;
const EUnexpectedOperation: u64 = 11;
const EInvalidBridgeRoute: u64 = 12;
const EInvariantSuiInitializedTokenTransferShouldNotBeClaimed: u64 = 13;
const EMessageNotFoundInRecords: u64 = 14;
const CURRENT_VERSION: u64 = 1;
struct TokenTransferApproved has copy, drop {
message_key: BridgeMessageKey,
}
struct TokenTransferClaimed has copy, drop {
message_key: BridgeMessageKey,
}
struct TokenTransferAlreadyApproved has copy, drop {
message_key: BridgeMessageKey,
}
struct TokenTransferAlreadyClaimed has copy, drop {
message_key: BridgeMessageKey,
}
fun init(ctx: &mut TxContext) {
let treasury = treasury::create(ctx);
let bridge_inner = BridgeInner {
bridge_version: CURRENT_VERSION,
// TODO: how do we make this configurable?
chain_id: sui_local_test(),
sequence_nums: vec_map::empty<u8, u64>(),
committee: committee::create(ctx),
treasury: treasury,
bridge_records: linked_table::new<BridgeMessageKey, BridgeRecord>(ctx),
frozen: false,
};
let bridge = Bridge {
id: object::new(ctx),
inner: versioned::create(CURRENT_VERSION, bridge_inner, ctx)
};
transfer::share_object(bridge);
}
fun load_inner_mut(
self: &mut Bridge,
): &mut BridgeInner {
let version = versioned::version(&self.inner);
// TODO: Replace this with a lazy update function when we add a new version of the inner object.
assert!(version == CURRENT_VERSION, EWrongInnerVersion);
let inner: &mut BridgeInner = versioned::load_value_mut(&mut self.inner);
assert!(inner.bridge_version == version, EWrongInnerVersion);
inner
}
#[allow(unused_function)] // TODO: remove annotation after implementing user-facing API
fun load_inner(
self: &Bridge,
): &BridgeInner {
let version = versioned::version(&self.inner);
// TODO: Replace this with a lazy update function when we add a new version of the inner object.
assert!(version == CURRENT_VERSION, EWrongInnerVersion);
let inner: &BridgeInner = versioned::load_value(&self.inner);
assert!(inner.bridge_version == version, EWrongInnerVersion);
inner
}
// THIS IS ONLY FOR TESTING, MUST NOT CHECK INTO PROD
public fun add_treasury_cap<T>(
self: &mut Bridge,
treasury_cap: TreasuryCap<T>,
) {
let inner = load_inner_mut(self);
treasury::add_treasury_cap(&mut inner.treasury, treasury_cap)
}
// Create bridge request to send token to other chain, the request will be in pending state until approved
public fun send_token<T>(
self: &mut Bridge,
target_chain: u8,
target_address: vector<u8>,
token: Coin<T>,
ctx: &mut TxContext
) {
let inner = load_inner_mut(self);
assert!(chain_ids::is_valid_route(inner.chain_id, target_chain), EInvalidBridgeRoute);
assert!(!inner.frozen, EBridgeUnavailable);
let bridge_seq_num = next_seq_num(inner, message_types::token());
let token_id = treasury::token_id<T>();
let token_amount = balance::value(coin::balance(&token));
// create bridge message
let message = message::create_token_bridge_message(
inner.chain_id,
bridge_seq_num,
address::to_bytes(tx_context::sender(ctx)),
target_chain,
target_address,
token_id,
token_amount,
);
// burn / escrow token, unsupported coins will fail in this step
treasury::burn(&mut inner.treasury, token, ctx);
// Store pending bridge request
let key = message::key(&message);
linked_table::push_back(&mut inner.bridge_records, key, BridgeRecord {
message,
verified_signatures: none(),
claimed: false,
});
// emit event
emit(TokenBridgeEvent {
message_type: message_types::token(),
seq_num: bridge_seq_num,
source_chain: inner.chain_id,
sender_address: address::to_bytes(tx_context::sender(ctx)),
target_chain,
target_address,
token_type: token_id,
amount: token_amount,
});
}
// Record bridge message approvals in Sui, called by the bridge client
// If already approved, return early instead of aborting.
public fun approve_bridge_message(
self: &mut Bridge,
message: BridgeMessage,
signatures: vector<vector<u8>>,
) {
let inner = load_inner_mut(self);
let key = message::key(&message);
// TODO: use borrow mut
// retrieve pending message if source chain is Sui, the initial message must exist on chain.
if (message::message_type(&message) == message_types::token() && message::source_chain(&message) == inner.chain_id) {
let record = linked_table::remove(&mut inner.bridge_records, key);
assert!(record.message == message, EMalformedMessageError);
assert!(!record.claimed, EInvariantSuiInitializedTokenTransferShouldNotBeClaimed);
// If record already has verified signatures, it means the message has been approved.
// Then we push this message back to bridge_records and exit early.
if (option::is_some(&record.verified_signatures)) {
emit(TokenTransferAlreadyApproved { message_key: key });
linked_table::push_back(&mut inner.bridge_records, key, record);
return
}
};
// At this point, if this message is in bridge_records, we know it's already approved
// because we only add a message to bridge_records after verifying the signatures.
if (linked_table::contains(&inner.bridge_records, key)) {
emit(TokenTransferAlreadyApproved { message_key: key });
return
};
// At this point, we know the message has not been approved, hence has not been claimed.
// verify signatures
committee::verify_signatures(&inner.committee, message, signatures);
// Critical: here we set `claimed` as false. It's vitally important to make sure
// the token transfer has not been claimed already.
// Store approval
linked_table::push_back(&mut inner.bridge_records, key, BridgeRecord {
message,
verified_signatures: some(signatures),
claimed: false
});
emit(TokenTransferApproved { message_key: key });
}
// Claim token from approved bridge message
// Returns Some(Coin) if coin can be claimed. If already claimed, return None
fun claim_token_internal<T>(
self: &mut Bridge,
source_chain: u8,
bridge_seq_num: u64,
ctx: &mut TxContext
): (Option<Coin<T>>, address) {
let inner = load_inner_mut(self);
let key = message::create_key(source_chain, message_types::token(), bridge_seq_num);
if (!linked_table::contains(&inner.bridge_records, key)) {
abort EMessageNotFoundInRecords
};
// TODO: use borrow mut
// retrieve approved bridge message
let BridgeRecord {
message,
verified_signatures: signatures,
claimed
} = linked_table::remove(&mut inner.bridge_records, key);
// ensure this is a token bridge message
assert!(message::message_type(&message) == message_types::token(), EUnexpectedMessageType);
// Ensure it's signed
assert!(option::is_some(&signatures), EUnauthorisedClaim);
// extract token message
let token_payload = message::extract_token_bridge_payload(&message);
// get owner address
let owner = address::from_bytes(message::token_target_address(&token_payload));
// If already claimed, exit early
if (claimed) {
emit(TokenTransferAlreadyClaimed { message_key: key });
linked_table::push_back(&mut inner.bridge_records, key, BridgeRecord {
message,
verified_signatures: signatures,
claimed: true // <-- this is important
});
return (option::none(), owner)
};
let target_chain = message::token_target_chain(&token_payload);
// ensure target chain matches self.chain_id
assert!(target_chain == inner.chain_id, EUnexpectedChainID);
// TODO: why do we check validity of the route here? what if inconsistency?
// Ensure route is valid
assert!(chain_ids::is_valid_route(source_chain, target_chain), EInvalidBridgeRoute);
// check token type
assert!(treasury::token_id<T>() == message::token_type(&token_payload), EUnexpectedTokenType);
// claim from treasury
let token = treasury::mint<T>(&mut inner.treasury, message::token_amount(&token_payload), ctx);
// Record changes
linked_table::push_back(&mut inner.bridge_records, key, BridgeRecord {
message,
verified_signatures: signatures,
claimed: true
});
emit(TokenTransferClaimed { message_key: key });
(option::some(token), owner)
}
// This function can only be called by the token recipient
// Returns None if the token has already been claimed.
public fun claim_token<T>(self: &mut Bridge, source_chain: u8, bridge_seq_num: u64, ctx: &mut TxContext): Option<Coin<T>> {
let (token, owner) = claim_token_internal<T>(self, source_chain, bridge_seq_num, ctx);
// Only token owner can claim the token
assert!(tx_context::sender(ctx) == owner, EUnauthorisedClaim);
token
}
// This function can be called by anyone to claim and transfer the token to the recipient
// If the token has already been claimed, it will return instead of aborting.
public fun claim_and_transfer_token<T>(
self: &mut Bridge,
source_chain: u8,
bridge_seq_num: u64,
ctx: &mut TxContext
) {
let (token, owner) = claim_token_internal<T>(self, source_chain, bridge_seq_num, ctx);
if (option::is_none(&token)) {
option::destroy_none(token);
return
};
transfer::public_transfer(option::destroy_some(token), owner)
}
public fun execute_emergency_op(
self: &mut Bridge,
message: BridgeMessage,
signatures: vector<vector<u8>>,
) {
assert!(message::message_type(&message) == message_types::emergency_op(), EUnexpectedMessageType);
let inner = load_inner_mut(self);
// check emergency ops seq number, emergency ops can only be executed in sequence order.
let emergency_op_seq_num = next_seq_num(inner, message_types::emergency_op());
assert!(message::seq_num(&message) == emergency_op_seq_num, EUnexpectedSeqNum);
committee::verify_signatures(&inner.committee, message, signatures);
let payload = message::extract_emergency_op_payload(&message);
if (message::emergency_op_type(&payload) == FREEZE) {
inner.frozen == true;
} else if (message::emergency_op_type(&payload) == UNFREEZE) {
inner.frozen == false;
} else {
abort EUnexpectedOperation
};
}
fun next_seq_num(self: &mut BridgeInner, msg_type: u8): u64 {
if (!vec_map::contains(&self.sequence_nums, &msg_type)) {
vec_map::insert(&mut self.sequence_nums, msg_type, 1);
return 0
};
let (key, seq_num) = vec_map::remove(&mut self.sequence_nums, &msg_type);
vec_map::insert(&mut self.sequence_nums, key, seq_num + 1);
seq_num
}
}
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
module bridge::btc {
use std::option;
use sui::coin;
use sui::transfer;
use sui::tx_context::{Self, TxContext};
friend bridge::treasury;
struct BTC has drop {}
fun init(witness: BTC, ctx: &mut TxContext) {
let (treasury_cap, metadata) = coin::create_currency(
witness,
8,
b"BTC",
b"Bitcoin",
b"Bridged Bitcoin token",
option::none(),
ctx
);
transfer::public_freeze_object(metadata);
transfer::public_transfer(treasury_cap, tx_context::sender(ctx));
}
}
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
module bridge::chain_ids {
use std::vector;
// Chain IDs
const SuiMainnet: u8 = 0;
const SuiTestnet: u8 = 1;
const SuiDevnet: u8 = 2;
const SuiLocalTest: u8 = 3;
const EthMainnet: u8 = 10;
const EthSepolia: u8 = 11;
const EthLocalTest: u8 = 12;
struct BridgeRoute has drop {
source: u8,
destination: u8,
}
public fun sui_mainnet(): u8 {
SuiMainnet
}
public fun sui_testnet(): u8 {
SuiTestnet
}
public fun sui_devnet(): u8 {
SuiDevnet
}
public fun sui_local_test(): u8 {
SuiLocalTest
}
public fun eth_mainnet(): u8 {
EthMainnet
}
public fun eth_sepolia(): u8 {
EthSepolia
}
public fun eth_local_test(): u8 {
EthLocalTest
}
public fun valid_routes(): vector<BridgeRoute> {
vector[
BridgeRoute { source: SuiMainnet, destination: EthMainnet },
BridgeRoute { source: SuiDevnet, destination: EthSepolia },
BridgeRoute { source: SuiTestnet, destination: EthSepolia },
BridgeRoute { source: SuiLocalTest, destination: EthLocalTest },
BridgeRoute { source: EthMainnet, destination: SuiMainnet },
BridgeRoute { source: EthSepolia, destination: SuiDevnet },
BridgeRoute { source: EthSepolia, destination: SuiTestnet },
BridgeRoute { source: EthLocalTest, destination: SuiLocalTest }]
}
public fun is_valid_route(source: u8, destination: u8): bool {
let route = BridgeRoute { source, destination };
return vector::contains(&valid_routes(), &route)
}
}
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
#[allow(unused_use)]
module bridge::committee {
use std::vector;
use sui::address;
use sui::ecdsa_k1;
use sui::hex;
use sui::tx_context::{Self, TxContext};
use sui::vec_map::{Self, VecMap};
use sui::vec_set;
use bridge::message::{Self, BridgeMessage};
use bridge::message_types;
friend bridge::bridge;
const ESignatureBelowThreshold: u64 = 0;
const EDuplicatedSignature: u64 = 1;
const EInvalidSignature: u64 = 2;
// const ENotSystemAddress: u64 = 3;
const SUI_MESSAGE_PREFIX: vector<u8> = b"SUI_BRIDGE_MESSAGE";
struct BridgeCommittee has store {
// commitee pub key and weight
members: VecMap<vector<u8>, CommitteeMember>,
// threshold for each message type
thresholds: VecMap<u8, u64>
}
struct CommitteeMember has drop, store {
/// The Sui Address of the validator
sui_address: address,
/// The public key bytes of the bridge key
bridge_pubkey_bytes: vector<u8>,
/// Voting power
voting_power: u64,
/// The HTTP REST URL the member's node listens to
/// it looks like b'https://127.0.0.1:9191'
http_rest_url: vector<u8>,
/// If this member is blocklisted
blocklisted: bool,
}
public(friend) fun create(_ctx: &TxContext): BridgeCommittee {
// assert!(tx_context::sender(ctx) == @0x0, ENotSystemAddress);
// Hardcoded genesis committee
// TODO: change this to real committe members
let members = vec_map::empty<vector<u8>, CommitteeMember>();
let bridge_pubkey_bytes = hex::decode(b"02321ede33d2c2d7a8a152f275a1484edef2098f034121a602cb7d767d38680aa4");
vec_map::insert(&mut members, bridge_pubkey_bytes, CommitteeMember {
// TODO: why do we need sui_address?
sui_address: address::from_u256(1),
bridge_pubkey_bytes,
voting_power: 2500,
http_rest_url: b"http://127.0.0.1:9191",
blocklisted: false
});
let bridge_pubkey_bytes = hex::decode(b"027f1178ff417fc9f5b8290bd8876f0a157a505a6c52db100a8492203ddd1d4279");
vec_map::insert(&mut members, bridge_pubkey_bytes, CommitteeMember {
sui_address: address::from_u256(2),
bridge_pubkey_bytes,
voting_power: 2500,
http_rest_url: b"http://127.0.0.1:9192",
blocklisted: false
});
let bridge_pubkey_bytes = hex::decode(b"026f311bcd1c2664c14277c7a80e4857c690626597064f89edc33b8f67b99c6bc0");
vec_map::insert(&mut members, bridge_pubkey_bytes, CommitteeMember {
sui_address: address::from_u256(3),
bridge_pubkey_bytes,
voting_power: 2500,
http_rest_url: b"http://127.0.0.1:9193",
blocklisted: false
});
let bridge_pubkey_bytes = hex::decode(b"03a57b85771aedeb6d31c808be9a6e73194e4b70e679608f2bca68bcc684773736");
vec_map::insert(&mut members, bridge_pubkey_bytes, CommitteeMember {
sui_address: address::from_u256(4),
bridge_pubkey_bytes,
voting_power: 2500,
http_rest_url: b"http://127.0.0.1:9194",
blocklisted: false
});
let thresholds = vec_map::empty();
vec_map::insert(&mut thresholds, message_types::token(), 3334);
BridgeCommittee { members, thresholds }
}
public fun verify_signatures(
self: &BridgeCommittee,
message: BridgeMessage,
signatures: vector<vector<u8>>,
) {
let (i, signature_counts) = (0, vector::length(&signatures));
let seen_pub_key = vec_set::empty<vector<u8>>();
let required_threshold = *vec_map::get(&self.thresholds, &message::message_type(&message));
// add prefix to the message bytes
let message_bytes = SUI_MESSAGE_PREFIX;
vector::append(&mut message_bytes, message::serialize_message(message));
let threshold = 0;
while (i < signature_counts) {
let signature = vector::borrow(&signatures, i);
let pubkey = ecdsa_k1::secp256k1_ecrecover(signature, &message_bytes, 0);
// check duplicate
assert!(!vec_set::contains(&seen_pub_key, &pubkey), EDuplicatedSignature);
// make sure pub key is part of the committee
assert!(vec_map::contains(&self.members, &pubkey), EInvalidSignature);
// get committee signature weight and check pubkey is part of the committee
let member = vec_map::get(&self.members, &pubkey);
if (!member.blocklisted) {
threshold = threshold + member.voting_power;
};
i = i + 1;
vec_set::insert(&mut seen_pub_key, pubkey);
};
assert!(threshold >= required_threshold, ESignatureBelowThreshold);
}
#[test_only]
const TEST_MSG: vector<u8> =
b"00010a0000000000000000200000000000000000000000000000000000000000000000000000000000000064012000000000000000000000000000000000000000000000000000000000000000c8033930000000000000";
#[test]
fun test_verify_signatures_good_path() {
let committee = setup_test();
let msg = message::deserialize_message(hex::decode(TEST_MSG));
// good path
verify_signatures(
&committee,
msg,
vector[hex::decode(
b"8ba030a450cb1e36f61e572645fc9da1dea5f79b6db663a21ab63286d7fc29af447433abdd0c0b35ab751154ac5b612ae64d3be810f0d9e10ff68e764514ced300"
), hex::decode(
b"439379cc7b3ee3ebe1ff59d011dafc1caac47da6919b089c90f6a24e8c284b963b20f1f5421385456e57ac6b69c4b5f0d345aa09b8bc96d88d87051c7349e83801"
)],
);
// Clean up
let BridgeCommittee {
members: _,
thresholds: _
} = committee;
}
#[test]
#[expected_failure(abort_code = EDuplicatedSignature)]
fun test_verify_signatures_duplicated_sig() {
let committee = setup_test();
let msg = message::deserialize_message(hex::decode(TEST_MSG));
// good path
verify_signatures(
&committee,
msg,
vector[hex::decode(
b"439379cc7b3ee3ebe1ff59d011dafc1caac47da6919b089c90f6a24e8c284b963b20f1f5421385456e57ac6b69c4b5f0d345aa09b8bc96d88d87051c7349e83801"
), hex::decode(
b"439379cc7b3ee3ebe1ff59d011dafc1caac47da6919b089c90f6a24e8c284b963b20f1f5421385456e57ac6b69c4b5f0d345aa09b8bc96d88d87051c7349e83801"
)],
);
abort 0
}
#[test]
#[expected_failure(abort_code = EInvalidSignature)]
fun test_verify_signatures_invalid_signature() {
let committee = setup_test();
let msg = message::deserialize_message(hex::decode(TEST_MSG));
// good path
verify_signatures(
&committee,
msg,
vector[hex::decode(
b"6ffb3e5ce04dd138611c49520fddfbd6778879c2db4696139f53a487043409536c369c6ffaca165ce3886723cfa8b74f3e043e226e206ea25e313ea2215e6caf01"
)],
);
abort 0
}
#[test]
#[expected_failure(abort_code = ESignatureBelowThreshold)]
fun test_verify_signatures_below_threshold() {
let committee = setup_test();
let msg = message::deserialize_message(hex::decode(TEST_MSG));
// good path
verify_signatures(
&committee,
msg,
vector[hex::decode(
b"439379cc7b3ee3ebe1ff59d011dafc1caac47da6919b089c90f6a24e8c284b963b20f1f5421385456e57ac6b69c4b5f0d345aa09b8bc96d88d87051c7349e83801"
)],
);
abort 0
}
#[test_only]
fun setup_test(): BridgeCommittee {
let members = vec_map::empty<vector<u8>, CommitteeMember>();
let bridge_pubkey_bytes = hex::decode(b"029bef8d556d80e43ae7e0becb3a7e6838b95defe45896ed6075bb9035d06c9964");
vec_map::insert(&mut members, bridge_pubkey_bytes, CommitteeMember {
sui_address: address::from_u256(1),
bridge_pubkey_bytes,
voting_power: 100,
http_rest_url: b"https://127.0.0.1:9191",
blocklisted: false
});
let bridge_pubkey_bytes = hex::decode(b"033e99a541db69bd32040dfe5037fbf5210dafa8151a71e21c5204b05d95ce0a62");
vec_map::insert(&mut members, bridge_pubkey_bytes, CommitteeMember {
sui_address: address::from_u256(2),
bridge_pubkey_bytes,
voting_power: 100,
http_rest_url: b"https://127.0.0.1:9192",
blocklisted: false
});
let thresholds = vec_map::empty<u8, u64>();
vec_map::insert(&mut thresholds, message_types::token(), 200);
let committee = BridgeCommittee {
members,
thresholds
};
committee
}
}
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
module bridge::eth {
use std::option;
use sui::coin;
use sui::transfer;
use sui::tx_context::{Self, TxContext};
friend bridge::treasury;
struct ETH has drop {}
fun init(witness: ETH, ctx: &mut TxContext) {
let (treasury_cap, metadata) = coin::create_currency(
witness,
// ETC DP limited to 8 on Sui
8,
b"ETH",
b"Ethereum",
b"Bridged Ethereum token",
option::none(),
ctx
);
transfer::public_freeze_object(metadata);
transfer::public_transfer(treasury_cap, tx_context::sender(ctx));
}
}
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
module bridge::message {
use std::vector;
use sui::bcs;
use sui::bcs::BCS;
use bridge::message_types;
#[test_only]
use bridge::chain_ids;
#[test_only]
use sui::address;
#[test_only]
use sui::balance;
#[test_only]
use sui::coin;
#[test_only]
use sui::hex;
#[test_only]
use sui::test_scenario;
struct USDC has drop {}
const CURRENT_MESSAGE_VERSION: u8 = 1;
const ETrailingBytes: u64 = 0;
struct BridgeMessage has copy, drop, store {
message_type: u8,
message_version: u8,
seq_num: u64,
source_chain: u8,
payload: vector<u8>
}
struct BridgeMessageKey has copy, drop, store {
source_chain: u8,
message_type: u8,
bridge_seq_num: u64
}
struct TokenPayload has drop {
sender_address: vector<u8>,
target_chain: u8,
target_address: vector<u8>,
token_type: u8,
amount: u64
}
struct EmergencyOp has drop {
op_type: u8
}
// Note: `bcs::peel_vec_u8` *happens* to work here because
// `sender_address` and `target_address` are no longer than 255 bytes.
// Therefore their length can be represented by a single byte.
// See `create_token_bridge_message` for the actual encoding rule.
public fun extract_token_bridge_payload(message: &BridgeMessage): TokenPayload {
let bcs = bcs::new(message.payload);
let sender_address = bcs::peel_vec_u8(&mut bcs);
let target_chain = bcs::peel_u8(&mut bcs);
let target_address = bcs::peel_vec_u8(&mut bcs);
let token_type = bcs::peel_u8(&mut bcs);
let amount = peel_u64_be(&mut bcs);
assert!(vector::is_empty(&bcs::into_remainder_bytes(bcs)), ETrailingBytes);
TokenPayload {
sender_address,
target_chain,
target_address,
token_type,
amount
}
}
public fun extract_emergency_op_payload(message: &BridgeMessage): EmergencyOp {
// emergency op payload is just a single byte
assert!(vector::length(&message.payload) == 1, ETrailingBytes);
EmergencyOp {
op_type: *vector::borrow(&message.payload, 0)
}
}
public fun serialize_message(message: BridgeMessage): vector<u8> {
let BridgeMessage {
message_type,
message_version,
seq_num,
source_chain,
payload
} = message;
let message = vector[];
vector::push_back(&mut message, message_type);
vector::push_back(&mut message, message_version);
// bcs serializes u64 as 8 bytes
vector::append(&mut message, reverse_bytes(bcs::to_bytes(&seq_num)));
vector::push_back(&mut message, source_chain);
vector::append(&mut message, payload);
message
}
/// Token Transfer Message Format:
/// [message_type: u8]
/// [version:u8]
/// [nonce:u64]
/// [source_chain: u8]
/// [sender_address_length:u8]
/// [sender_address: byte[]]
/// [target_chain:u8]
/// [target_address_length:u8]
/// [target_address: byte[]]
/// [token_type:u8]
/// [amount:u64]
public fun create_token_bridge_message(
source_chain: u8,
seq_num: u64,
sender_address: vector<u8>,
target_chain: u8,
target_address: vector<u8>,
token_type: u8,
amount: u64
): BridgeMessage {
let payload = vector[];
// sender address should be less than 255 bytes so can fit into u8
vector::push_back(&mut payload, (vector::length(&sender_address) as u8));
vector::append(&mut payload, sender_address);
vector::push_back(&mut payload, target_chain);
// target address should be less than 255 bytes so can fit into u8
vector::push_back(&mut payload, (vector::length(&target_address) as u8));
vector::append(&mut payload, target_address);
vector::push_back(&mut payload, token_type);
// bcs serialzies u64 as 8 bytes
vector::append(&mut payload, reverse_bytes(bcs::to_bytes(&amount)));
BridgeMessage {
message_type: message_types::token(),
message_version: CURRENT_MESSAGE_VERSION,
seq_num,
source_chain,
payload,
}
}
/// Emergency Op Message Format:
/// [message_type: u8]
/// [version:u8]
/// [nonce:u64]
/// [chain_id: u8]
/// [op_type: u8]
public fun create_emergency_op_message(
source_chain: u8,
seq_num: u64,
op_type: u8,
): BridgeMessage {
BridgeMessage {
message_type: message_types::emergency_op(),
message_version: CURRENT_MESSAGE_VERSION,
seq_num,
source_chain,
payload: vector[op_type],
}
}
public fun create_key(source_chain: u8, message_type: u8, bridge_seq_num: u64): BridgeMessageKey {
BridgeMessageKey { source_chain, message_type, bridge_seq_num }
}
public fun key(self: &BridgeMessage): BridgeMessageKey {
create_key(self.source_chain, self.message_type, self.seq_num)
}
// BridgeMessage getters
public fun message_type(self: &BridgeMessage): u8 {
self.message_type
}
public fun seq_num(self: &BridgeMessage): u64 {
self.seq_num
}
// TokenBridgePayload getters
public fun source_chain(self: &BridgeMessage): u8 {
self.source_chain
}
public fun token_target_chain(self: &TokenPayload): u8 {
self.target_chain
}
public fun token_target_address(self: &TokenPayload): vector<u8> {
self.target_address
}
public fun token_type(self: &TokenPayload): u8 {
self.token_type
}
public fun token_amount(self: &TokenPayload): u64 {
self.amount
}
// EmergencyOpPayload getters
public fun emergency_op_type(self: &EmergencyOp): u8 {
self.op_type
}
fun reverse_bytes(bytes: vector<u8>): vector<u8> {
vector::reverse(&mut bytes);
bytes
}
fun peel_u64_be(bcs: &mut BCS): u64 {
let (value, i) = (0u64, 64u8);
while (i > 0) {
i = i - 8;
let byte = (bcs::peel_u8(bcs) as u64);
value = value + (byte << i);
};
value
}
#[test_only]
public fun deserialize_message(message: vector<u8>): BridgeMessage {
let bcs = bcs::new(message);
BridgeMessage {
message_type: bcs::peel_u8(&mut bcs),
message_version: bcs::peel_u8(&mut bcs),
seq_num: peel_u64_be(&mut bcs),
source_chain: bcs::peel_u8(&mut bcs),
payload: bcs::into_remainder_bytes(bcs)
}
}
#[test]
fun test_message_serialization_sui_to_eth() {
let sender_address = address::from_u256(100);
let scenario = test_scenario::begin(sender_address);
let ctx = test_scenario::ctx(&mut scenario);
let coin = coin::mint_for_testing<USDC>(12345, ctx);
let token_bridge_message = create_token_bridge_message(
chain_ids::sui_testnet(), // source chain
10, // seq_num
address::to_bytes(sender_address), // sender address
chain_ids::eth_sepolia(), // target_chain
// Eth address is 20 bytes long
hex::decode(b"00000000000000000000000000000000000000c8"), // target_address
3u8, // token_type
balance::value(coin::balance(&coin)) // amount: u64
);
// Test payload extraction
let token_payload = TokenPayload {
sender_address: address::to_bytes(sender_address),
target_chain: chain_ids::eth_sepolia(),
target_address: hex::decode(b"00000000000000000000000000000000000000c8"),
token_type: 3u8,
amount: balance::value(coin::balance(&coin))
};
assert!(extract_token_bridge_payload(&token_bridge_message) == token_payload, 0);
// Test message serialization
let message = serialize_message(token_bridge_message);
let expected_msg = hex::decode(
b"0001000000000000000a012000000000000000000000000000000000000000000000000000000000000000640b1400000000000000000000000000000000000000c8030000000000003039",
);
assert!(message == expected_msg, 0);
assert!(token_bridge_message == deserialize_message(message), 0);
coin::burn_for_testing(coin);
test_scenario::end(scenario);
}
#[test]
fun test_message_serialization_eth_to_sui() {
let address_1 = address::from_u256(100);
let scenario = test_scenario::begin(address_1);
let ctx = test_scenario::ctx(&mut scenario);
let coin = coin::mint_for_testing<USDC>(12345, ctx);
let token_bridge_message = create_token_bridge_message(
chain_ids::eth_sepolia(), // source chain
10, // seq_num
// Eth address is 20 bytes long
hex::decode(b"00000000000000000000000000000000000000c8"), // eth sender address
chain_ids::sui_testnet(), // target_chain
address::to_bytes(address_1), // target address
3u8, // token_type
balance::value(coin::balance(&coin)) // amount: u64
);
// Test payload extraction
let token_payload = TokenPayload {
sender_address: hex::decode(b"00000000000000000000000000000000000000c8"),
target_chain: chain_ids::sui_testnet(),
target_address: address::to_bytes(address_1),
token_type: 3u8,
amount: balance::value(coin::balance(&coin))
};
assert!(extract_token_bridge_payload(&token_bridge_message) == token_payload, 0);
// Test message serialization
let message = serialize_message(token_bridge_message);
let expected_msg = hex::decode(
b"0001000000000000000a0b1400000000000000000000000000000000000000c801200000000000000000000000000000000000000000000000000000000000000064030000000000003039",
);
assert!(message == expected_msg, 0);
assert!(token_bridge_message == deserialize_message(message), 0);
coin::burn_for_testing(coin);
test_scenario::end(scenario);
}
#[test]
fun test_be_to_le_conversion() {
let input = hex::decode(b"78563412");
let expected = hex::decode(b"12345678");
assert!(reverse_bytes(input) == expected, 0)
}
#[test]
fun test_peel_u64_be() {
let input = hex::decode(b"0000000000003039");
let expected = 12345u64;
let bcs = bcs::new(input);
assert!(peel_u64_be(&mut bcs) == expected, 0)
}
}
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
module bridge::message_types {
// message types
const TOKEN: u8 = 0;
const COMMITTEE_BLOCKLIST: u8 = 1;
const EMERGENCY_OP: u8 = 2;
//const COMMITTEE_CHANGE: u8 = 2;
//const NFT: u8 = 4;
public fun token():u8{
TOKEN
}
public fun committee_blocklist():u8{
COMMITTEE_BLOCKLIST
}
public fun emergency_op():u8{
EMERGENCY_OP
}
}
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
module bridge::treasury {
use std::type_name;
use sui::coin::{Self, Coin};
use sui::object_bag::{Self, ObjectBag};
use sui::tx_context::TxContext;
use bridge::btc::BTC;
use bridge::eth::ETH;
use bridge::usdc::USDC;
use bridge::usdt::USDT;
use sui::coin::TreasuryCap;
friend bridge::bridge;
const EUnsupportedTokenType: u64 = 0;
// const ENotSystemAddress: u64 = 1;
struct BridgeTreasury has store {
treasuries: ObjectBag
}
public(friend) fun add_treasury_cap<T>(self: &mut BridgeTreasury, treasury_cap: TreasuryCap<T>) {
let type = type_name::get<T>();
object_bag::add(&mut self.treasuries, type, treasury_cap)
}
public(friend) fun create(ctx: &mut TxContext): BridgeTreasury {
// assert!(tx_context::sender(ctx) == @0x0, ENotSystemAddress);
BridgeTreasury {
treasuries: object_bag::new(ctx)
}
}
public(friend) fun burn<T>(self: &mut BridgeTreasury, token: Coin<T>, ctx: &TxContext) {
// public(friend) fun burn<T>(self: &mut BridgeTreasury, token: Coin<T>, ctx: &mut TxContext) {
create_treasury_if_not_exist<T>(self, ctx);
let treasury = object_bag::borrow_mut(&mut self.treasuries, type_name::get<T>());
coin::burn(treasury, token);
}
public(friend) fun mint<T>(self: &mut BridgeTreasury, amount: u64, ctx: &mut TxContext): Coin<T> {
create_treasury_if_not_exist<T>(self, ctx);
let treasury = object_bag::borrow_mut(&mut self.treasuries, type_name::get<T>());
coin::mint(treasury, amount, ctx)
}
// fun create_treasury_if_not_exist<T>(self: &mut BridgeTreasury, ctx: &mut TxContext) {
fun create_treasury_if_not_exist<T>(self: &BridgeTreasury, _ctx: &TxContext) {
let type = type_name::get<T>();
if (!object_bag::contains(&self.treasuries, type)) {
// // Lazily create currency if not exists
// if (type == type_name::get<BTC>()) {
// object_bag::add(&mut self.treasuries, type, btc::create(ctx));
// } else if (type == type_name::get<ETH>()) {
// object_bag::add(&mut self.treasuries, type, eth::create(ctx));
// } else if (type == type_name::get<USDC>()) {
// object_bag::add(&mut self.treasuries, type, usdc::create(ctx));
// } else if (type == type_name::get<USDT>()) {
// object_bag::add(&mut self.treasuries, type, usdt::create(ctx));
// } else {
abort EUnsupportedTokenType
// };
};
}
public fun token_id<T>(): u8 {
let coin_type = type_name::get<T>();
if (coin_type == type_name::get<BTC>()) {
1
} else if (coin_type == type_name::get<ETH>()) {
2
} else if (coin_type == type_name::get<USDC>()) {
3
} else if (coin_type == type_name::get<USDT>()) {
4
} else {
abort EUnsupportedTokenType
}
}
}
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
module bridge::usdc {
use std::option;
use sui::coin;
use sui::transfer;
use sui::tx_context::{Self, TxContext};
friend bridge::treasury;
struct USDC has drop {}
fun init(witness: USDC, ctx: &mut TxContext) {
let (treasury_cap, metadata) = coin::create_currency(
witness,
6,
b"USDC",
b"USD Coin",
b"Bridged USD Coin token",
option::none(),
ctx
);
transfer::public_freeze_object(metadata);
transfer::public_transfer(treasury_cap, tx_context::sender(ctx));
}
}
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
module bridge::usdt {
use std::option;
use sui::coin;
use sui::transfer;
use sui::tx_context::{Self, TxContext};
friend bridge::treasury;
struct USDT has drop {}
fun init(witness: USDT, ctx: &mut TxContext) {
let (treasury_cap, metadata) = coin::create_currency(
witness,
6,
b"USDT",
b"Tether",
b"Bridged Tether token",
option::none(),
ctx
);
transfer::public_freeze_object(metadata);
transfer::public_transfer(treasury_cap, tx_context::sender(ctx));
}
}
[package]
name = "dynamic_field_sample"
version = "0.0.1"
[dependencies]
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir="crates/sui-framework/packages/sui-framework/", rev = "testnet" }
[addresses]
dynamic_field_sample = "0x0"
module dynamic_field_sample::dynamic_field {
use sui::dynamic_field;
use sui::object::{Self, UID};
use sui::transfer;
use sui::tx_context::{Self, TxContext};
struct Parent has key {
id: UID,
}
struct Child has key, store {
id: UID,
count: u64,
}
public entry fun create_child(ctx: &mut TxContext) {
transfer::public_transfer(
Child { id: object::new(ctx), count: 0 },tx_context::sender(ctx),
);
}
public entry fun create_parent(ctx: &mut TxContext) {
let parent = Parent {
id: object::new(ctx),
};
transfer::transfer(parent, tx_context::sender(ctx));
}
public entry fun add_child(parent: &mut Parent, child: Child) {
dynamic_field::add(&mut parent.id, 123, child);
}
public entry fun remove_child(parent: &mut Parent, ctx: &mut TxContext) {
let child: Child = dynamic_field::remove(&mut parent.id, 123);
transfer::transfer(child, tx_context::sender(ctx));
}
public entry fun mutate_child(parent: &mut Parent) {
let child = dynamic_field::borrow_mut<u64, Child>(&mut parent.id, 123);
child.count = child.count + 1;
}
}
[package]
name = "dynamic_object_field"
version = "0.0.1"
[dependencies]
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir="crates/sui-framework/packages/sui-framework/", rev = "testnet" }
[addresses]
dynamic_object_field = "0x0"
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
module dynamic_object_field::ownership {
use sui::dynamic_object_field as ofield;
use sui::object::{Self, UID};
use sui::transfer;
use sui::tx_context::{Self, TxContext};
struct Parent has key {
id: UID,
}
struct Child has key, store {
id: UID,
count: u64
}
public entry fun create_parent(ctx: &mut TxContext) {
let parent = Parent {
id: object::new(ctx),
};
transfer::transfer(parent, tx_context::sender(ctx));
}
public entry fun create_child(ctx: &mut TxContext) {
let child = Child {
id: object::new(ctx),
count: 0
};
transfer::transfer(child, tx_context::sender(ctx));
}
// wrong way
public entry fun transfer_child_by_parent_object_address(parent: address, child: Child) {
transfer::transfer(child, parent);
}
public entry fun transfer_child_by_dynamic_object_field(parent: &mut Parent, child: Child) {
ofield::add(&mut parent.id, b"child", child);
}
public entry fun mutate_child(child: &mut Child) {
child.count = child.count + 1;
}
public entry fun mutate_child_via_parent(parent: &mut Parent) {
mutate_child(ofield::borrow_mut<vector<u8>, Child>(
&mut parent.id,
b"child",
));
}
public entry fun delete_child(parent: &mut Parent) {
let Child { id, count: _ } = ofield::remove<vector<u8>, Child>(
&mut parent.id,
b"child",
);
object::delete(id);
}
public entry fun reclaim_child(parent: &mut Parent, ctx: &mut TxContext) {
let child = ofield::remove<vector<u8>, Child>(
&mut parent.id,
b"child",
);
transfer::transfer(child, tx_context::sender(ctx));
}
}
# @generated by Move, please check-in and do not edit manually.
[move]
version = 0
dependencies = [
{ name = "Sui" },
]
[[move.package]]
name = "MoveStdlib"
source = { git = "https://github.com/MystenLabs/sui.git", rev = "testnet", subdir = "crates/sui-framework/packages/move-stdlib" }
[[move.package]]
name = "Sui"
source = { git = "https://github.com/MystenLabs/sui.git", rev = "testnet", subdir = "crates/sui-framework/packages/sui-framework/" }
dependencies = [
{ name = "MoveStdlib" },
]
[package]
name = "my_first_package"
version = "0.0.1"
[dependencies]
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir="crates/sui-framework/packages/sui-framework/", rev = "testnet" }
[addresses]
my_first_package = "0x0"
sui = "0000000000000000000000000000000000000000000000000000000000000002"
module my_first_package::my_module {
// Part 1: Imports
use sui::object::{Self, UID};
use sui::transfer;
use sui::tx_context::{Self, TxContext};
// Part 2: Struct definitions
struct Sword has key, store {
id: UID,
magic: u64,
strength: u64,
}
struct Forge has key, store {
id: UID,
swords_created: u64,
}
// Part 3: Module initializer to be executed when this module is published
fun init(ctx: &mut TxContext) {
let admin = Forge {
id: object::new(ctx),
swords_created: 0,
};
// Transfer the forge object to the module/package publisher
transfer::transfer(admin, tx_context::sender(ctx));
}
// Part 4: Accessors required to read the struct attributes
public fun magic(self: &Sword): u64 {
self.magic
}
public fun strength(self: &Sword): u64 {
self.strength
}
public fun swords_created(self: &Forge): u64 {
self.swords_created
}
public entry fun sword_create(forge: &mut Forge,magic: u64, strength: u64, recipient: address, ctx: &mut TxContext) {
use sui::transfer;
// create a sword
let sword = Sword {
id: object::new(ctx),
magic: magic,
strength: strength,
};
// transfer the sword
transfer::transfer(sword, recipient);
forge.swords_created = forge.swords_created + 1;
}
public entry fun sword_transfer(sword: Sword, recipient: address, _ctx: &mut TxContext) {
use sui::transfer;
// transfer the sword
transfer::transfer(sword, recipient);
}
// Part 5: Public/entry functions (introduced later in the tutorial)
// Part 6: Private functions (if any)
#[test]
public fun test_sword_create() {
use sui::tx_context;
use sui::transfer;
// Create a dummy TxContext for testing
let ctx = tx_context::dummy();
// Create a sword
let sword = Sword {
id: object::new(&mut ctx),
magic: 42,
strength: 7,
};
// Check if accessor functions return correct values
assert!(magic(&sword) == 42 && strength(&sword) == 7, 1);
let dummy_address = @0xCAFE;
transfer::transfer(sword, dummy_address);
}
#[test]
fun test_sword_transactions() {
use sui::test_scenario;
use sui::transfer;
// create test addresses representing users
let admin = @0xBABE;
let initial_owner = @0xCAFE;
let final_owner = @0xFACE;
// first transaction to emulate module initialization
let scenario_val = test_scenario::begin(admin);
let scenario = &mut scenario_val;
{
init(test_scenario::ctx(scenario));
};
// second transaction executed by admin to create the sword
test_scenario::next_tx(scenario, admin);
{
let forge = test_scenario::take_from_sender<Forge>(scenario);
// create the sword and transfer it to the initial owner
sword_create(&mut forge, 42, 7, initial_owner, test_scenario::ctx(scenario));
transfer::transfer(forge, admin);
};
// third transaction executed by the initial sword owner
test_scenario::next_tx(scenario, initial_owner);
{
// extract the sword owned by the initial owner
let sword = test_scenario::take_from_sender<Sword>(scenario);
// transfer the sword to the final owner
sword_transfer(sword, final_owner, test_scenario::ctx(scenario))
};
// fourth transaction executed by the final sword owner
test_scenario::next_tx(scenario, final_owner);
{
// extract the sword owned by the final owner
let sword = test_scenario::take_from_sender<Sword>(scenario);
// verify that the sword has expected properties
assert!(magic(&sword) == 42 && strength(&sword) == 7, 1);
// return the sword to the object pool (it cannot be simply "dropped")
test_scenario::return_to_sender(scenario, sword)
};
test_scenario::end(scenario_val);
}
#[test]
public fun test_module_init() {
use sui::test_scenario;
// Create test address representing game admin
let admin = @0xBABE;
// First transaction to emulate module initialization
let scenario_val = test_scenario::begin(admin);
let scenario = &mut scenario_val;
{
init(test_scenario::ctx(scenario));
};
// Second transaction to check if the forge has been created
// and has initial value of zero swords created
test_scenario::next_tx(scenario, admin);
{
// Extract the Forge object
let forge = test_scenario::take_from_sender<Forge>(scenario);
// Verify number of created swords
assert!(swords_created(&forge) == 0, 1);
// Return the Forge object to the object pool
test_scenario::return_to_sender(scenario, forge);
};
test_scenario::end(scenario_val);
}
}
[package]
name = "FungibleTokens"
version = "0.0.1"
[dependencies]
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir="crates/sui-framework/packages/sui-framework/", rev = "devnet" }
[addresses]
fungible_tokens = "0x0"
abc = "0x0"
rc = "0x0"

Fungible Tokens

  • MANAGED: a token managed by a treasurer trusted for minting and burning. This is how (e.g.) a fiat-backed stablecoin or an in-game virtual currency would work.
  • BASKET: a synthetic token backed by a basket of other assets. This how (e.g.) a Special Drawing Rights (SDR)-like asset would work.
  • REGULATED_COIN: a coin managed by a central authority which can freeze accounts
  • PRIVATE_COIN: a coin which has the option of hiding the amount that you transact
  • PRIVATE_BALANCE: a balance which has the option of hiding the amount that it stores.
  • FIXED: a token with a fixed supply (coming in future).
  • ALGO: a token with an algorithmic issuance policy (coming in future).
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
/// A synthetic fungible token backed by a basket of other tokens.
/// Here, we use a basket that is 1:1 SUI and MANAGED,
/// but this approach would work for a basket with arbitrary assets/ratios.
/// E.g., [SDR](https://www.imf.org/en/About/Factsheets/Sheets/2016/08/01/14/51/Special-Drawing-Right-SDR)
/// could be implemented this way.
module fungible_tokens::basket {
use fungible_tokens::managed::MANAGED;
use sui::coin::{Self, Coin};
use sui::balance::{Self, Balance, Supply};
use sui::object::{Self, UID};
use sui::sui::SUI;
use sui::transfer;
use sui::tx_context::TxContext;
/// Name of the coin. By convention, this type has the same name as its parent module
/// and has no fields. The full type of the coin defined by this module will be `COIN<BASKET>`.
struct BASKET has drop { }
/// Singleton shared object holding the reserve assets and the capability.
struct Reserve has key {
id: UID,
/// capability allowing the reserve to mint and burn BASKET
total_supply: Supply<BASKET>,
/// SUI coins held in the reserve
sui: Balance<SUI>,
/// MANAGED coins held in the reserve
managed: Balance<MANAGED>,
}
/// Needed to deposit a 1:1 ratio of SUI and MANAGED for minting, but deposited a different ratio
const EBadDepositRatio: u64 = 0;
fun init(witness: BASKET, ctx: &mut TxContext) {
// Get a treasury cap for the coin put it in the reserve
let total_supply = balance::create_supply<BASKET>(witness);
transfer::share_object(Reserve {
id: object::new(ctx),
total_supply,
sui: balance::zero<SUI>(),
managed: balance::zero<MANAGED>(),
})
}
/// === Writes ===
/// Mint BASKET coins by accepting an equal number of SUI and MANAGED coins
public fun mint(
reserve: &mut Reserve, sui: Coin<SUI>, managed: Coin<MANAGED>, ctx: &mut TxContext
): Coin<BASKET> {
let num_sui = coin::value(&sui);
assert!(num_sui == coin::value(&managed), EBadDepositRatio);
coin::put(&mut reserve.sui, sui);
coin::put(&mut reserve.managed, managed);
let minted_balance = balance::increase_supply(&mut reserve.total_supply, num_sui);
coin::from_balance(minted_balance, ctx)
}
/// Burn BASKET coins and return the underlying reserve assets
public fun burn(
reserve: &mut Reserve, basket: Coin<BASKET>, ctx: &mut TxContext
): (Coin<SUI>, Coin<MANAGED>) {
let num_basket = balance::decrease_supply(&mut reserve.total_supply, coin::into_balance(basket));
let sui = coin::take(&mut reserve.sui, num_basket, ctx);
let managed = coin::take(&mut reserve.managed, num_basket, ctx);
(sui, managed)
}
// === Reads ===
/// Return the number of `MANAGED` coins in circulation
public fun total_supply(reserve: &Reserve): u64 {
balance::supply_value(&reserve.total_supply)
}
/// Return the number of SUI in the reserve
public fun sui_supply(reserve: &Reserve): u64 {
balance::value(&reserve.sui)
}
/// Return the number of MANAGED in the reserve
public fun managed_supply(reserve: &Reserve): u64 {
balance::value(&reserve.managed)
}
#[test_only]
public fun init_for_testing(ctx: &mut TxContext) {
init(BASKET {}, ctx)
}
}
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
/// Example coin with a trusted manager responsible for minting/burning (e.g., a stablecoin)
/// By convention, modules defining custom coin types use upper case names, in contrast to
/// ordinary modules, which use camel case.
module fungible_tokens::managed {
use std::option;
use sui::coin::{Self, Coin, TreasuryCap};
use sui::transfer;
use sui::tx_context::{Self, TxContext};
/// Name of the coin. By convention, this type has the same name as its parent module
/// and has no fields. The full type of the coin defined by this module will be `COIN<MANAGED>`.
struct MANAGED has drop {}
/// Register the managed currency to acquire its `TreasuryCap`. Because
/// this is a module initializer, it ensures the currency only gets
/// registered once.
fun init(witness: MANAGED, ctx: &mut TxContext) {
// Get a treasury cap for the coin and give it to the transaction sender
let (treasury_cap, metadata) = coin::create_currency<MANAGED>(witness, 2, b"MANAGED", b"", b"", option::none(), ctx);
transfer::public_freeze_object(metadata);
transfer::public_transfer(treasury_cap, tx_context::sender(ctx))
}
/// Manager can mint new coins
public entry fun mint(
treasury_cap: &mut TreasuryCap<MANAGED>, amount: u64, recipient: address, ctx: &mut TxContext
) {
coin::mint_and_transfer(treasury_cap, amount, recipient, ctx)
}
/// Manager can burn coins
public entry fun burn(treasury_cap: &mut TreasuryCap<MANAGED>, coin: Coin<MANAGED>) {
coin::burn(treasury_cap, coin);
}
#[test_only]
/// Wrapper of module initializer for testing
public fun test_init(ctx: &mut TxContext) {
init(MANAGED {}, ctx)
}
}
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
/// Module representing a common type for regulated coins. Features balance
/// accessors which can be used to implement a RegulatedCoin interface.
///
/// To implement any of the methods, module defining the type for the currency
/// is expected to implement the main set of methods such as `borrow()`,
/// `borrow_mut()` and `zero()`.
///
/// Each of the methods of this module requires a Witness struct to be sent.
module rc::regulated_coin {
use sui::balance::{Self, Balance};
use sui::tx_context::TxContext;
use sui::object::{Self, UID};
/// The RegulatedCoin struct; holds a common `Balance<T>` which is compatible
/// with all the other Coins and methods, as well as the `creator` field, which
/// can be used for additional security/regulation implementations.
struct RegulatedCoin<phantom T> has key, store {
id: UID,
balance: Balance<T>,
creator: address
}
/// Get the `RegulatedCoin.balance.value` field;
public fun value<T>(c: &RegulatedCoin<T>): u64 {
balance::value(&c.balance)
}
/// Get the `RegulatedCoin.creator` field;
public fun creator<T>(c: &RegulatedCoin<T>): address {
c.creator
}
// === Necessary set of Methods (provide security guarantees and balance access) ===
/// Get an immutable reference to the Balance of a RegulatedCoin;
public fun borrow<T: drop>(_: T, coin: &RegulatedCoin<T>): &Balance<T> {
&coin.balance
}
/// Get a mutable reference to the Balance of a RegulatedCoin;
public fun borrow_mut<T: drop>(_: T, coin: &mut RegulatedCoin<T>): &mut Balance<T> {
&mut coin.balance
}
/// Author of the currency can restrict who is allowed to create new balances;
public fun zero<T: drop>(_: T, creator: address, ctx: &mut TxContext): RegulatedCoin<T> {
RegulatedCoin { id: object::new(ctx), balance: balance::zero(), creator }
}
/// Build a transferable `RegulatedCoin` from a `Balance`;
public fun from_balance<T: drop>(
_: T, balance: Balance<T>, creator: address, ctx: &mut TxContext
): RegulatedCoin<T> {
RegulatedCoin { id: object::new(ctx), balance, creator }
}
/// Destroy `RegulatedCoin` and return its `Balance`;
public fun into_balance<T: drop>(_: T, coin: RegulatedCoin<T>): Balance<T> {
let RegulatedCoin { balance, creator: _, id } = coin;
sui::object::delete(id);
balance
}
// === Optional Methods (can be used for simpler implementation of basic operations) ===
/// Join Balances of a `RegulatedCoin` c1 and `RegulatedCoin` c2.
public fun join<T: drop>(witness: T, c1: &mut RegulatedCoin<T>, c2: RegulatedCoin<T>) {
balance::join(&mut c1.balance, into_balance(witness, c2));
}
/// Subtract `RegulatedCoin` with `value` from `RegulatedCoin`.
///
/// This method does not provide any checks by default and can possibly lead to mocking
/// behavior of `Regulatedcoin::zero()` when a value is 0. So in case empty balances
/// should not be allowed, this method should be additionally protected against zero value.
public fun split<T: drop>(
witness: T, c1: &mut RegulatedCoin<T>, creator: address, value: u64, ctx: &mut TxContext
): RegulatedCoin<T> {
let balance = balance::split(&mut c1.balance, value);
from_balance(witness, balance, creator, ctx)
}
}
/// Abc is a RegulatedCoin which:
///
/// - is managed account creation (only admins can create a new balance)
/// - has a denylist for addresses managed by the coin admins
/// - has restricted transfers which can not be taken by anyone except the recipient
module abc::abc {
use rc::regulated_coin::{Self as rcoin, RegulatedCoin as RCoin};
use sui::tx_context::{Self, TxContext};
use sui::balance::{Self, Supply, Balance};
use sui::object::{Self, UID};
use sui::coin::{Self, Coin};
use sui::transfer;
use std::vector;
/// The ticker of Abc regulated token
struct Abc has drop {}
/// A restricted transfer of Abc to another account.
struct Transfer has key {
id: UID,
balance: Balance<Abc>,
to: address,
}
/// A registry of addresses banned from using the coin.
struct Registry has key {
id: UID,
banned: vector<address>,
swapped_amount: u64,
}
/// A AbcTreasuryCap for the balance::Supply.
struct AbcTreasuryCap has key, store {
id: UID,
supply: Supply<Abc>
}
/// For when an attempting to interact with another account's RegulatedCoin<Abc>.
const ENotOwner: u64 = 1;
/// For when address has been banned and someone is trying to access the balance
const EAddressBanned: u64 = 2;
/// Create the Abc currency and send the AbcTreasuryCap to the creator
/// as well as the first (and empty) balance of the RegulatedCoin<Abc>.
///
/// Also creates a shared Registry which holds banned addresses.
fun init(ctx: &mut TxContext) {
let sender = tx_context::sender(ctx);
let treasury_cap = AbcTreasuryCap {
id: object::new(ctx),
supply: balance::create_supply(Abc {})
};
transfer::public_transfer(zero(sender, ctx), sender);
transfer::public_transfer(treasury_cap, sender);
transfer::share_object(Registry {
id: object::new(ctx),
banned: vector::empty(),
swapped_amount: 0,
});
}
// === Getters section: Registry ===
/// Get total amount of `Coin` from the `Registry`.
public fun swapped_amount(r: &Registry): u64 {
r.swapped_amount
}
/// Get vector of banned addresses from `Registry`.
public fun banned(r: &Registry): &vector<address> {
&r.banned
}
// === Admin actions: creating balances, minting coins and banning addresses ===
/// Create an empty `RCoin<Abc>` instance for account `for`. AbcTreasuryCap is passed for
/// authentication purposes - only admin can create new accounts.
public entry fun create(_: &AbcTreasuryCap, for: address, ctx: &mut TxContext) {
transfer::public_transfer(zero(for, ctx), for)
}
/// Mint more Abc. Requires AbcTreasuryCap for authorization, so can only be done by admins.
public entry fun mint(treasury: &mut AbcTreasuryCap, owned: &mut RCoin<Abc>, value: u64) {
balance::join(borrow_mut(owned), balance::increase_supply(&mut treasury.supply, value));
}
/// Burn `value` amount of `RCoin<Abc>`. Requires AbcTreasuryCap for authorization, so can only be done by admins.
///
/// TODO: Make AbcTreasuryCap a part of Balance module instead of Coin.
public entry fun burn(treasury: &mut AbcTreasuryCap, owned: &mut RCoin<Abc>, value: u64) {
balance::decrease_supply(
&mut treasury.supply,
balance::split(borrow_mut(owned), value)
);
}
/// Ban some address and forbid making any transactions from or to this address.
/// Only owner of the AbcTreasuryCap can perform this action.
public entry fun ban(_cap: &AbcTreasuryCap, registry: &mut Registry, to_ban: address) {
vector::push_back(&mut registry.banned, to_ban)
}
// === Public: Regulated transfers ===
/// Transfer entrypoint - create a restricted `Transfer` instance and transfer it to the
/// `to` account for being accepted later.
/// Fails if sender is not an creator of the `RegulatedCoin` or if any of the parties is in
/// the ban list in Registry.
public entry fun transfer(r: &Registry, coin: &mut RCoin<Abc>, value: u64, to: address, ctx: &mut TxContext) {
let sender = tx_context::sender(ctx);
assert!(rcoin::creator(coin) == sender, ENotOwner);
assert!(vector::contains(&r.banned, &to) == false, EAddressBanned);
assert!(vector::contains(&r.banned, &sender) == false, EAddressBanned);
transfer::transfer(Transfer {
to,
id: object::new(ctx),
balance: balance::split(borrow_mut(coin), value),
}, to)
}
/// Accept an incoming transfer by joining an incoming balance with an owned one.
///
/// Fails if:
/// 1. the `RegulatedCoin<Abc>.creator` does not match `Transfer.to`;
/// 2. the address of the creator/recipient is banned;
public entry fun accept_transfer(r: &Registry, coin: &mut RCoin<Abc>, transfer: Transfer) {
let Transfer { id, balance, to } = transfer;
assert!(rcoin::creator(coin) == to, ENotOwner);
assert!(vector::contains(&r.banned, &to) == false, EAddressBanned);
balance::join(borrow_mut(coin), balance);
object::delete(id)
}
// === Public: Swap RegulatedCoin <-> Coin ===
/// Take `value` amount of `RegulatedCoin` and make it freely transferable by wrapping it into
/// a `Coin`. Update `Registry` to keep track of the swapped amount.
///
/// Fails if:
/// 1. `RegulatedCoin<Abc>.creator` was banned;
/// 2. `RegulatedCoin<Abc>` is not owned by the tx sender;
public entry fun take(r: &mut Registry, coin: &mut RCoin<Abc>, value: u64, ctx: &mut TxContext) {
let sender = tx_context::sender(ctx);
assert!(rcoin::creator(coin) == sender, ENotOwner);
assert!(vector::contains(&r.banned, &sender) == false, EAddressBanned);
// Update swapped amount for Registry to keep track of non-regulated amounts.
r.swapped_amount = r.swapped_amount + value;
transfer::public_transfer(coin::take(borrow_mut(coin), value, ctx), sender);
}
/// Take `Coin` and put to the `RegulatedCoin`'s balance.
///
/// Fails if:
/// 1. `RegulatedCoin<Abc>.creator` was banned;
/// 2. `RegulatedCoin<Abc>` is not owned by the tx sender;
public entry fun put_back(r: &mut Registry, rc_coin: &mut RCoin<Abc>, coin: Coin<Abc>, ctx: &TxContext) {
let balance = coin::into_balance(coin);
let sender = tx_context::sender(ctx);
assert!(rcoin::creator(rc_coin) == sender, ENotOwner);
assert!(vector::contains(&r.banned, &sender) == false, EAddressBanned);
// Update swapped amount as in `swap_regulated`.
r.swapped_amount = r.swapped_amount - balance::value(&balance);
balance::join(borrow_mut(rc_coin), balance);
}
// === Private implementations accessors and type morphing ===
fun borrow(coin: &RCoin<Abc>): &Balance<Abc> { rcoin::borrow(Abc {}, coin) }
fun borrow_mut(coin: &mut RCoin<Abc>): &mut Balance<Abc> { rcoin::borrow_mut(Abc {}, coin) }
fun zero(creator: address, ctx: &mut TxContext): RCoin<Abc> { rcoin::zero(Abc {}, creator, ctx) }
fun into_balance(coin: RCoin<Abc>): Balance<Abc> { rcoin::into_balance(Abc {}, coin) }
fun from_balance(balance: Balance<Abc>, creator: address, ctx: &mut TxContext): RCoin<Abc> {
rcoin::from_balance(Abc {}, balance, creator, ctx)
}
// === Testing utilities ===
#[test_only] public fun init_for_testing(ctx: &mut TxContext) { init(ctx) }
#[test_only] public fun borrow_for_testing(coin: &RCoin<Abc>): &Balance<Abc> { borrow(coin) }
#[test_only] public fun borrow_mut_for_testing(coin: &mut RCoin<Abc>): &Balance<Abc> { borrow_mut(coin) }
}
#[test_only]
/// Tests for the abc module. They are sequential and based on top of each other.
/// ```
/// * - test_minting
/// | +-- test_creation
/// | +-- test_transfer
/// | +-- test_burn
/// | +-- test_take
/// | +-- test_put_back
/// | +-- test_ban
/// | +-- test_address_banned_fail
/// | +-- test_different_account_fail
/// | +-- test_not_owned_balance_fail
/// ```
module abc::tests {
use abc::abc::{Self, Abc, AbcTreasuryCap, Registry};
use rc::regulated_coin::{Self as rcoin, RegulatedCoin as RCoin};
use sui::coin::{Coin};
use sui::test_scenario::{Self, Scenario, next_tx, ctx};
// === Test handlers; this trick helps reusing scenarios ==
fun test_minting() {
let scenario = scenario();
test_minting_(&mut scenario);
test_scenario::end(scenario);
}
fun test_creation() {
let scenario = scenario();
test_creation_(&mut scenario);
test_scenario::end(scenario);
}
fun test_transfer() {
let scenario = scenario();
test_transfer_(&mut scenario);
test_scenario::end(scenario);
}
fun test_burn() {
let scenario = scenario();
test_burn_(&mut scenario);
test_scenario::end(scenario);
}
fun test_take() {
let scenario = scenario();
test_take_(&mut scenario);
test_scenario::end(scenario);
}
fun test_put_back() {
let scenario = scenario();
test_put_back_(&mut scenario);
test_scenario::end(scenario);
}
fun test_ban() {
let scenario = scenario();
test_ban_(&mut scenario);
test_scenario::end(scenario);
}
#[test]
#[expected_failure(abort_code = abc::abc::EAddressBanned)]
fun test_address_banned_fail() {
let scenario = scenario();
test_address_banned_fail_(&mut scenario);
test_scenario::end(scenario);
}
#[test]
#[expected_failure(abort_code = abc::abc::EAddressBanned)]
fun test_different_account_fail() {
let scenario = scenario();
test_different_account_fail_(&mut scenario);
test_scenario::end(scenario);
}
#[test]
#[expected_failure(abort_code = abc::abc::ENotOwner)]
fun test_not_owned_balance_fail() {
let scenario = scenario();
test_not_owned_balance_fail_(&mut scenario);
test_scenario::end(scenario);
}
// === Helpers and basic test organization ===
fun scenario(): Scenario { test_scenario::begin(@0xAbc) }
fun people(): (address, address, address) { (@0xAbc, @0xE05, @0xFACE) }
// Admin creates a regulated coin Abc and mints 1,000,000 of it.
fun test_minting_(test: &mut Scenario) {
let (admin, _, _) = people();
next_tx(test, admin);
{
abc::init_for_testing(ctx(test))
};
next_tx(test, admin);
{
let cap = test_scenario::take_from_sender<AbcTreasuryCap>(test);
let coin = test_scenario::take_from_sender<RCoin<Abc>>(test);
abc::mint(&mut cap, &mut coin, 1000000);
assert!(rcoin::value(&coin) == 1000000, 0);
test_scenario::return_to_sender(test, cap);
test_scenario::return_to_sender(test, coin);
}
}
// Admin creates an empty balance for the `user1`.
fun test_creation_(test: &mut Scenario) {
let (admin, user1, _) = people();
test_minting_(test);
next_tx(test, admin);
{
let cap = test_scenario::take_from_sender<AbcTreasuryCap>(test);
abc::create(&cap, user1, ctx(test));
test_scenario::return_to_sender(test, cap);
};
next_tx(test, user1);
{
let coin = test_scenario::take_from_sender<RCoin<Abc>>(test);
assert!(rcoin::creator(&coin) == user1, 1);
assert!(rcoin::value(&coin) == 0, 2);
test_scenario::return_to_sender(test, coin);
};
}
// Admin transfers 500,000 coins to `user1`.
// User1 accepts the transfer and checks his balance.
fun test_transfer_(test: &mut Scenario) {
let (admin, user1, _) = people();
test_creation_(test);
next_tx(test, admin);
{
let coin = test_scenario::take_from_sender<RCoin<Abc>>(test);
let reg = test_scenario::take_shared<Registry>(test);
let reg_ref = &mut reg;
abc::transfer(reg_ref, &mut coin, 500000, user1, ctx(test));
test_scenario::return_shared(reg);
test_scenario::return_to_sender(test, coin);
};
next_tx(test, user1);
{
let coin = test_scenario::take_from_sender<RCoin<Abc>>(test);
let transfer = test_scenario::take_from_sender<abc::Transfer>(test);
let reg = test_scenario::take_shared<Registry>(test);
let reg_ref = &mut reg;
abc::accept_transfer(reg_ref, &mut coin, transfer);
assert!(rcoin::value(&coin) == 500000, 3);
test_scenario::return_shared(reg);
test_scenario::return_to_sender(test, coin);
};
}
// Admin burns 100,000 of `RCoin<Abc>`
fun test_burn_(test: &mut Scenario) {
let (admin, _, _) = people();
test_transfer_(test);
next_tx(test, admin);
{
let coin = test_scenario::take_from_sender<RCoin<Abc>>(test);
let treasury_cap = test_scenario::take_from_sender<AbcTreasuryCap>(test);
abc::burn(&mut treasury_cap, &mut coin, 100000);
assert!(rcoin::value(&coin) == 400000, 4);
test_scenario::return_to_sender(test, treasury_cap);
test_scenario::return_to_sender(test, coin);
};
}
// User1 cashes 100,000 of his `RegulatedCoin` into a `Coin`;
// User1 sends Coin<Abc> it to `user2`.
fun test_take_(test: &mut Scenario) {
let (_, user1, user2) = people();
test_transfer_(test);
next_tx(test, user1);
{
let coin = test_scenario::take_from_sender<RCoin<Abc>>(test);
let reg = test_scenario::take_shared<Registry>(test);
let reg_ref = &mut reg;
abc::take(reg_ref, &mut coin, 100000, ctx(test));
assert!(abc::swapped_amount(reg_ref) == 100000, 5);
assert!(rcoin::value(&coin) == 400000, 5);
test_scenario::return_shared( reg);
test_scenario::return_to_sender(test, coin);
};
next_tx(test, user1);
{
let coin = test_scenario::take_from_sender<Coin<Abc>>(test);
sui::transfer::public_transfer(coin, user2);
};
}
// User2 sends his `Coin<Abc>` to `admin`.
// Admin puts this coin to his RegulatedCoin balance.
fun test_put_back_(test: &mut Scenario) {
let (admin, _, user2) = people();
test_take_(test);
next_tx(test, user2);
{
let coin = test_scenario::take_from_sender<Coin<Abc>>(test);
sui::transfer::public_transfer(coin, admin);
};
next_tx(test, admin);
{
let coin = test_scenario::take_from_sender<Coin<Abc>>(test);
let reg_coin = test_scenario::take_from_sender<RCoin<Abc>>(test);
let reg = test_scenario::take_shared<Registry>(test);
let reg_ref = &mut reg;
abc::put_back(reg_ref, &mut reg_coin, coin, ctx(test));
test_scenario::return_to_sender(test, reg_coin);
test_scenario::return_shared(reg);
}
}
// Admin bans user1 by adding his address to the registry.
fun test_ban_(test: &mut Scenario) {
let (admin, user1, _) = people();
test_transfer_(test);
next_tx(test, admin);
{
let cap = test_scenario::take_from_sender<AbcTreasuryCap>(test);
let reg = test_scenario::take_shared<Registry>(test);
let reg_ref = &mut reg;
abc::ban(&cap, reg_ref, user1);
test_scenario::return_shared(reg);
test_scenario::return_to_sender(test, cap);
};
}
// Banned User1 fails to create a Transfer.
fun test_address_banned_fail_(test: &mut Scenario) {
let (_, user1, user2) = people();
test_ban_(test);
next_tx(test, user1);
{
let coin = test_scenario::take_from_sender<RCoin<Abc>>(test);
let reg = test_scenario::take_shared<Registry>(test);
let reg_ref = &mut reg;
abc::transfer(reg_ref, &mut coin, 250000, user2, ctx(test));
test_scenario::return_shared(reg);
test_scenario::return_to_sender(test, coin);
};
}
// User1 is banned. Admin tries to make a Transfer to User1 and fails - user banned.
fun test_different_account_fail_(test: &mut Scenario) {
let (admin, user1, _) = people();
test_ban_(test);
next_tx(test, admin);
{
let coin = test_scenario::take_from_sender<RCoin<Abc>>(test);
let reg = test_scenario::take_shared<Registry>(test);
let reg_ref = &mut reg;
abc::transfer(reg_ref, &mut coin, 250000, user1, ctx(test));
test_scenario::return_shared(reg);
test_scenario::return_to_sender(test, coin);
};
}
// User1 is banned and transfers the whole balance to User2.
// User2 tries to use this balance and fails.
fun test_not_owned_balance_fail_(test: &mut Scenario) {
let (_, user1, user2) = people();
test_ban_(test);
next_tx(test, user1);
{
let coin = test_scenario::take_from_sender<RCoin<Abc>>(test);
sui::transfer::public_transfer(coin, user2);
};
next_tx(test, user2);
{
let coin = test_scenario::take_from_sender<RCoin<Abc>>(test);
let reg = test_scenario::take_shared<Registry>(test);
let reg_ref = &mut reg;
abc::transfer(reg_ref, &mut coin, 500000, user1, ctx(test));
test_scenario::return_shared(reg);
test_scenario::return_to_sender(test, coin);
}
}
}
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
/// WARNING: Like all files in the examples section, this code is unaudited
/// and should NOT be running in production. Using the code unaudited could potentially
/// result in lost of funds from hacks, and leakage of transaction amounts.
/// An example implementation of a 'treasury lock'. It encapsulates the TreasuryCap
/// of a Coin so that additional whitelisted parties (bearers of the `MintCap`)
/// can mint new Coins up to a pre-defined per epoch limit. This can be used e.g.
/// to create a faucet.
module fungible_tokens::treasury_lock {
use sui::object::{Self, UID, ID};
use sui::coin::{Self, TreasuryCap};
use sui::balance::{Balance};
use sui::tx_context::{Self, TxContext};
use sui::transfer;
use sui::vec_set::{Self, VecSet};
/// This mint capability instance is banned.
const EMintCapBanned: u64 = 0;
/// Requested mint amount exceeds the per epoch mint limit.
const EMintAmountTooLarge: u64 = 1;
/// Encapsulates the `TreasuryCap` and stores the list of banned mint authorities.
struct TreasuryLock<phantom T> has key {
id: UID,
treasury_cap: TreasuryCap<T>,
banned_mint_authorities: VecSet<ID>
}
/// Admin capability for `TreasuryLock`. Bearer has the power to create, ban,
/// and unban mint capabilities (`MintCap`)
struct LockAdminCap<phantom T> has key, store {
id: UID
}
/// Capability allowing the bearer to mint new Coins up to a pre-defined per epoch limit.
struct MintCap<phantom T> has key, store {
id: UID,
max_mint_per_epoch: u64,
last_epoch: u64,
minted_in_epoch: u64
}
/// Create a new `TreasuryLock` for `TreasuryCap`.
public fun new_lock<T>(
cap: TreasuryCap<T>, ctx: &mut TxContext
): LockAdminCap<T> {
let lock = TreasuryLock {
id: object::new(ctx),
treasury_cap: cap,
banned_mint_authorities: vec_set::empty<ID>()
};
transfer::share_object(lock);
LockAdminCap<T> {
id: object::new(ctx),
}
}
/// Entry function. Creates a new `TreasuryLock` for `TreasuryCap`. Invokes `new_lock`.
public entry fun new_lock_<T>(cap: TreasuryCap<T>, ctx: &mut TxContext) {
transfer::public_transfer(
new_lock(cap, ctx),
tx_context::sender(ctx)
)
}
/// Create a new mint capability whose bearer will be allowed to mint
/// `max_mint_per_epoch` coins per epoch.
public fun create_mint_cap<T>(
_cap: &LockAdminCap<T>, max_mint_per_epoch: u64, ctx: &mut TxContext
): MintCap<T> {
MintCap<T>{
id: object::new(ctx),
max_mint_per_epoch,
last_epoch: tx_context::epoch(ctx),
minted_in_epoch: 0
}
}
/// Entry function. Creates a new mint capability whose bearer will be allowed
/// to mint `max_mint_per_epoch` coins per epoch. Sends it to `recipient`.
public fun create_and_transfer_mint_cap<T>(
cap: &LockAdminCap<T>, max_mint_per_epoch: u64, recipient: address, ctx: &mut TxContext
) {
transfer::public_transfer(
create_mint_cap(cap, max_mint_per_epoch, ctx),
recipient
)
}
/// Ban a `MintCap`.
public fun ban_mint_cap_id<T>(
_cap: &LockAdminCap<T>, lock: &mut TreasuryLock<T>, id: ID
) {
vec_set::insert(&mut lock.banned_mint_authorities, id)
}
/// Entry function. Bans a `MintCap`.
public entry fun ban_mint_cap_id_<T>(
cap: &LockAdminCap<T>, lock: &mut TreasuryLock<T>, id: ID
) {
ban_mint_cap_id(cap, lock, id);
}
/// Unban a previously banned `MintCap`.
public fun unban_mint_cap_id<T>(
_cap: &LockAdminCap<T>, lock: &mut TreasuryLock<T>, id: ID
) {
vec_set::remove(&mut lock.banned_mint_authorities, &id)
}
/// Entry function. Unbans a previously banned `MintCap`.
public entry fun unban_mint_cap_id_<T>(
cap: &LockAdminCap<T>, lock: &mut TreasuryLock<T>, id: ID
) {
unban_mint_cap_id(cap, lock, id);
}
/// Borrow the `TreasuryCap` to use directly.
public fun treasury_cap_mut<T>(
_cap: &LockAdminCap<T>, lock: &mut TreasuryLock<T>
): &mut TreasuryCap<T> {
&mut lock.treasury_cap
}
/// Mint a `Balance` from a `TreasuryLock` providing a `MintCap`.
public fun mint_balance<T>(
lock: &mut TreasuryLock<T>, cap: &mut MintCap<T>, amount: u64, ctx: &mut TxContext
): Balance<T> {
assert!(
!vec_set::contains(&lock.banned_mint_authorities, object::uid_as_inner(&cap.id)),
EMintCapBanned
);
let epoch = tx_context::epoch(ctx);
if (cap.last_epoch != epoch) {
cap.last_epoch = epoch;
cap.minted_in_epoch = 0;
};
assert!(
cap.minted_in_epoch + amount <= cap.max_mint_per_epoch,
EMintAmountTooLarge
);
cap.minted_in_epoch = cap.minted_in_epoch + amount;
coin::mint_balance(&mut lock.treasury_cap, amount)
}
/// Entry function. Mint a `Coin` from a `TreasuryLock` providing a `MintCap`
/// and transfer it to recipient.
public entry fun mint_and_transfer<T>(
lock: &mut TreasuryLock<T>,
cap: &mut MintCap<T>,
amount: u64,
recipient: address,
ctx: &mut TxContext
) {
let balance = mint_balance(lock, cap, amount, ctx);
transfer::public_transfer(
coin::from_balance(balance, ctx),
recipient
)
}
}
#[test_only]
module fungible_tokens::treasury_lock_tests {
use std::option;
use sui::test_scenario::{Self, Scenario};
use sui::balance::{Self, Balance};
use sui::transfer;
use sui::coin;
use sui::object::{Self};
use sui::test_utils;
use fungible_tokens::treasury_lock::{Self, TreasuryLock, LockAdminCap, MintCap, create_and_transfer_mint_cap, new_lock, mint_balance};
const ADMIN: address = @0xABBA;
const USER: address = @0xB0B;
// one time witness for the coin used in tests
struct TREASURY_LOCK_TESTS has drop {}
fun user_with_mint_cap_scenario(): Scenario {
let scenario_ = test_scenario::begin(ADMIN);
let scenario = &mut scenario_;
// create a currency and lock it
test_scenario::next_tx(scenario, ADMIN);
{
let treasury_lock_tests = test_utils::create_one_time_witness<TREASURY_LOCK_TESTS>();
let (treasury, metadata) = coin::create_currency(treasury_lock_tests, 0, b"", b"", b"", option::none(), test_scenario::ctx(scenario));
transfer::public_freeze_object(metadata);
let admin_cap = new_lock(treasury, test_scenario::ctx(scenario));
transfer::public_transfer(
admin_cap,
ADMIN
)
};
// create a mint capability and transfer it to user
test_scenario::next_tx(scenario, ADMIN);
{
let admin_cap = test_scenario::take_from_sender<LockAdminCap<TREASURY_LOCK_TESTS>>(scenario);
create_and_transfer_mint_cap(&admin_cap, 500, USER, test_scenario::ctx(scenario));
test_scenario::return_to_sender(scenario, admin_cap);
};
test_scenario::next_tx(scenario, ADMIN);
return scenario_
}
fun user_mint_balance(scenario: &mut Scenario, amount: u64): Balance<TREASURY_LOCK_TESTS> {
let mint_cap = test_scenario::take_from_sender<MintCap<TREASURY_LOCK_TESTS>>(scenario);
let lock = test_scenario::take_shared<TreasuryLock<TREASURY_LOCK_TESTS>>(scenario);
let balance = mint_balance(
&mut lock,
&mut mint_cap,
amount,
test_scenario::ctx(scenario)
);
test_scenario::return_to_sender(scenario, mint_cap);
test_scenario::return_shared(lock);
balance
}
#[test]
fun test_user_can_mint() {
let scenario_ = user_with_mint_cap_scenario();
let scenario = &mut scenario_;
// user uses its capability to mint 300 coins
test_scenario::next_tx(scenario, USER);
{
let balance = user_mint_balance(scenario, 300);
assert!(balance::value<TREASURY_LOCK_TESTS>(&balance) == 300, 0);
transfer::public_transfer(
coin::from_balance(balance, test_scenario::ctx(scenario)),
USER
);
};
test_scenario::end(scenario_);
}
#[test]
#[expected_failure(abort_code = treasury_lock::EMintAmountTooLarge)]
fun test_minting_over_limit_fails() {
let scenario_ = user_with_mint_cap_scenario();
let scenario = &mut scenario_;
// mint 300 coins
test_scenario::next_tx(scenario, USER);
{
let balance = user_mint_balance(scenario, 300);
assert!(balance::value<TREASURY_LOCK_TESTS>(&balance) == 300, 0);
transfer::public_transfer(
coin::from_balance(balance, test_scenario::ctx(scenario)),
USER
);
};
// mint 200 more
test_scenario::next_tx(scenario, USER);
{
let balance = user_mint_balance(scenario, 200);
assert!(balance::value<TREASURY_LOCK_TESTS>(&balance) == 200, 0);
transfer::public_transfer(
coin::from_balance(balance, test_scenario::ctx(scenario)),
USER
);
};
// attempt to mint amount over the epoch limit - should fail
test_scenario::next_tx(scenario, USER);
{
let balance = user_mint_balance(scenario, 1);
transfer::public_transfer(
coin::from_balance(balance, test_scenario::ctx(scenario)),
USER
);
};
test_scenario::end(scenario_);
}
#[test]
fun test_minted_amount_resets_at_epoch_change() {
let scenario_ = user_with_mint_cap_scenario();
let scenario = &mut scenario_;
// mint 300 coins
test_scenario::next_tx(scenario, USER);
{
let balance = user_mint_balance(scenario, 300);
assert!(balance::value<TREASURY_LOCK_TESTS>(&balance) == 300, 0);
transfer::public_transfer(
coin::from_balance(balance, test_scenario::ctx(scenario)),
USER
);
};
// next epoch and mint 300 again
test_scenario::next_epoch(scenario, USER);
{
let balance = user_mint_balance(scenario, 300);
transfer::public_transfer(
coin::from_balance(balance, test_scenario::ctx(scenario)),
USER
);
};
test_scenario::end(scenario_);
}
#[test]
#[expected_failure(abort_code = treasury_lock::EMintCapBanned)]
fun test_banned_cap_cannot_mint() {
let scenario_ = user_with_mint_cap_scenario();
let scenario = &mut scenario_;
// get the mint cap ID for reference
test_scenario::next_tx(scenario, USER);
let mint_cap = test_scenario::take_from_sender<MintCap<TREASURY_LOCK_TESTS>>(scenario);
let mint_cap_id = object::id(&mint_cap);
test_scenario::return_to_sender(scenario, mint_cap);
// mint 100 coins
test_scenario::next_tx(scenario, USER);
{
let balance = user_mint_balance(scenario, 100);
assert!(balance::value<TREASURY_LOCK_TESTS>(&balance) == 100, 0);
transfer::public_transfer(
coin::from_balance(balance, test_scenario::ctx(scenario)),
USER
);
};
// admin bans mint cap
test_scenario::next_tx(scenario, ADMIN);
{
let admin_cap = test_scenario::take_from_sender<LockAdminCap<TREASURY_LOCK_TESTS>>(scenario);
let lock = test_scenario::take_shared<TreasuryLock<TREASURY_LOCK_TESTS>>(scenario);
treasury_lock::ban_mint_cap_id(
&admin_cap,
&mut lock,
mint_cap_id
);
test_scenario::return_to_sender(scenario, admin_cap);
test_scenario::return_shared(lock);
};
// user attempts to mint but fails
test_scenario::next_tx(scenario, USER);
{
let balance = user_mint_balance(scenario, 100);
transfer::public_transfer(
coin::from_balance(balance, test_scenario::ctx(scenario)),
USER
);
};
test_scenario::end(scenario_);
}
#[test]
fun test_user_can_mint_after_unban() {
let scenario_ = user_with_mint_cap_scenario();
let scenario = &mut scenario_;
// get the mint cap ID for reference
test_scenario::next_tx(scenario, USER);
let mint_cap = test_scenario::take_from_sender<MintCap<TREASURY_LOCK_TESTS>>(scenario);
let mint_cap_id = object::id(&mint_cap);
test_scenario::return_to_sender(scenario, mint_cap);
// mint 100 coins
test_scenario::next_tx(scenario, USER);
{
let balance = user_mint_balance(scenario, 100);
assert!(balance::value<TREASURY_LOCK_TESTS>(&balance) == 100, 0);
transfer::public_transfer(
coin::from_balance(balance, test_scenario::ctx(scenario)),
USER
);
};
// admin bans mint cap
test_scenario::next_tx(scenario, ADMIN);
{
let admin_cap = test_scenario::take_from_sender<LockAdminCap<TREASURY_LOCK_TESTS>>(scenario);
let lock = test_scenario::take_shared<TreasuryLock<TREASURY_LOCK_TESTS>>(scenario);
treasury_lock::ban_mint_cap_id(
&admin_cap,
&mut lock,
mint_cap_id
);
test_scenario::return_to_sender(scenario, admin_cap);
test_scenario::return_shared(lock);
};
// admin unbans mint cap
test_scenario::next_tx(scenario, ADMIN);
{
let admin_cap = test_scenario::take_from_sender<LockAdminCap<TREASURY_LOCK_TESTS>>(scenario);
let lock = test_scenario::take_shared<TreasuryLock<TREASURY_LOCK_TESTS>>(scenario);
treasury_lock::unban_mint_cap_id(
&admin_cap,
&mut lock,
mint_cap_id
);
test_scenario::return_to_sender(scenario, admin_cap);
test_scenario::return_shared(lock);
};
// user can mint
test_scenario::next_tx(scenario, USER);
{
let balance = user_mint_balance(scenario, 100);
assert!(balance::value<TREASURY_LOCK_TESTS>(&balance) == 100, 0);
transfer::public_transfer(
coin::from_balance(balance, test_scenario::ctx(scenario)),
USER
);
};
test_scenario::end(scenario_);
}
}
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
#[test_only]
module fungible_tokens::basket_tests {
use fungible_tokens::basket::{Self, Reserve};
use fungible_tokens::managed::MANAGED;
use sui::pay;
use sui::coin;
use sui::sui::SUI;
use sui::test_scenario;
#[test]
public fun test_mint_burn() {
let user = @0xA;
let scenario_val = test_scenario::begin(user);
let scenario = &mut scenario_val;
{
let ctx = test_scenario::ctx(scenario);
basket::init_for_testing(ctx);
};
test_scenario::next_tx(scenario, user);
{
let reserve_val = test_scenario::take_shared<Reserve>(scenario);
let reserve = &mut reserve_val;
let ctx = test_scenario::ctx(scenario);
assert!(basket::total_supply(reserve) == 0, 0);
let num_coins = 10;
let sui = coin::mint_for_testing<SUI>(num_coins, ctx);
let managed = coin::mint_for_testing<MANAGED>(num_coins, ctx);
let basket = basket::mint(reserve, sui, managed, ctx);
assert!(coin::value(&basket) == num_coins, 1);
assert!(basket::total_supply(reserve) == num_coins, 2);
let (sui, managed) = basket::burn(reserve, basket, ctx);
assert!(coin::value(&sui) == num_coins, 3);
assert!(coin::value(&managed) == num_coins, 4);
pay::keep(sui, ctx);
pay::keep(managed, ctx);
test_scenario::return_shared(reserve_val);
};
test_scenario::end(scenario_val);
}
}
[package]
name = "CoinSample"
version = "0.0.1"
[dependencies]
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir="crates/sui-framework/packages/sui-framework/", rev = "testnet" }
[addresses]
coin_sample = "0x0"
module coin_sample::mycoin {
use std::option;
use sui::coin::{Self, Coin, TreasuryCap};
use sui::transfer;
use sui::tx_context::{Self, TxContext};
#[test_only]
use sui::test_scenario;
struct MYCOIN has drop {}
fun init(witness: MYCOIN, ctx: &mut TxContext) {
let (treasury_cap, metadata)
= coin::create_currency<MYCOIN>(
witness,
2, // decimals
b"MC", // symbol
b"MYCOIN", // name
b"my coin", // description
option::none(),
ctx
);
transfer::public_freeze_object(metadata);
transfer::public_transfer(treasury_cap, tx_context::sender(ctx))
}
public entry fun mint(
treasury_cap: &mut TreasuryCap<MYCOIN>, amount:u64, recipient: address, ctx: &mut TxContext
) {
coin::mint_and_transfer(treasury_cap, amount, recipient, ctx)
}
public entry fun burn(treasury_cap: &mut TreasuryCap<MYCOIN>, coin: Coin<MYCOIN>) {
coin::burn(treasury_cap, coin);
}
#[test]
public fun test_mint() {
let user = @0xA;
let receiver = @0xB;
let num_coins = 10;
let scenario_val = test_scenario::begin(user);
let scenario = &mut scenario_val;
{
let ctx = test_scenario::ctx(scenario);
init(MYCOIN {}, ctx)
};
test_scenario::next_tx(scenario, user);
{
let treasuryCap = test_scenario::take_from_sender<TreasuryCap<MYCOIN>>(scenario);
mint(&mut treasuryCap, num_coins, receiver, test_scenario::ctx(scenario));
test_scenario::return_to_sender(scenario, treasuryCap);
};
test_scenario::next_tx(scenario, receiver);
{
let mycoin = test_scenario::take_from_sender<Coin<MYCOIN>>(scenario);
assert!(coin::value(&mycoin) == num_coins, 3);
test_scenario::return_to_sender(scenario, mycoin);
};
test_scenario::end(scenario_val);
}
#[test]
public fun test_burn() {
let user = @0xA;
let num_coins = 10;
let scenario_val = test_scenario::begin(user);
let scenario = &mut scenario_val;
{
let ctx = test_scenario::ctx(scenario);
init(MYCOIN {}, ctx)
};
test_scenario::next_tx(scenario, user);
{
let treasuryCap = test_scenario::take_from_sender<TreasuryCap<MYCOIN>>(scenario);
mint(&mut treasuryCap, num_coins, user, test_scenario::ctx(scenario));
test_scenario::return_to_sender(scenario, treasuryCap);
};
test_scenario::next_tx(scenario, user);
{
let treasuryCap = test_scenario::take_from_sender<TreasuryCap<MYCOIN>>(scenario);
let mycoin = test_scenario::take_from_sender<Coin<MYCOIN>>(scenario);
let supply_before_burn = coin::total_supply(&mut treasuryCap);
assert!(supply_before_burn == 10, 3);
burn(&mut treasuryCap, mycoin);
let supply_after_burn = coin::total_supply(&mut treasuryCap);
assert!(supply_after_burn == 0, 3);
test_scenario::return_to_sender(scenario, treasuryCap);
};
test_scenario::end(scenario_val);
}
}
[package]
name = "Tutorial"
version = "0.0.1"
[dependencies]
MoveStdlib = { git = "https://github.com/MystenLabs/sui.git", subdir="crates/sui-framework/packages/move-stdlib/", rev = "testnet" }
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir="crates/sui-framework/packages/sui-framework/", rev = "testnet" }
[addresses]
tutorial = "0x0"
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
module tutorial::color_object {
use sui::object::{Self, UID};
use sui::transfer;
use sui::tx_context::{Self, TxContext};
struct ColorObject has key {
id: UID,
red: u8,
green: u8,
blue: u8,
}
// == Functions covered in Chapter 1 ==
fun new(red: u8, green: u8, blue: u8, ctx: &mut TxContext): ColorObject {
ColorObject {
id: object::new(ctx),
red,
green,
blue,
}
}
public entry fun create(red: u8, green: u8, blue: u8, ctx: &mut TxContext) {
let color_object = new(red, green, blue, ctx);
transfer::transfer(color_object, tx_context::sender(ctx))
}
public fun get_color(self: &ColorObject): (u8, u8, u8) {
(self.red, self.green, self.blue)
}
// == Functions covered in Chapter 2 ==
/// Copies the values of `from_object` into `into_object`.
public entry fun copy_into(from_object: &ColorObject, into_object: &mut ColorObject) {
into_object.red = from_object.red;
into_object.green = from_object.green;
into_object.blue = from_object.blue;
}
public entry fun delete(object: ColorObject) {
let ColorObject { id, red: _, green: _, blue: _ } = object;
object::delete(id);
}
public entry fun transfer(object: ColorObject, recipient: address) {
transfer::transfer(object, recipient)
}
// == Functions covered in Chapter 3 ==
public entry fun freeze_object(object: ColorObject) {
transfer::freeze_object(object)
}
public entry fun create_immutable(red: u8, green: u8, blue: u8, ctx: &mut TxContext) {
let color_object = new(red, green, blue, ctx);
transfer::freeze_object(color_object)
}
public entry fun update(
object: &mut ColorObject,
red: u8, green: u8, blue: u8,
) {
object.red = red;
object.green = green;
object.blue = blue;
}
}
#[test_only]
module tutorial::color_object_tests {
use sui::test_scenario;
use tutorial::color_object::{Self, ColorObject};
use sui::object;
use sui::tx_context;
// == Tests covered in Chapter 1 ==
#[test]
fun test_create() {
let owner = @0x1;
// Create a ColorObject and transfer it to @owner.
let scenario_val = test_scenario::begin(owner);
let scenario = &mut scenario_val;
{
let ctx = test_scenario::ctx(scenario);
color_object::create(255, 0, 255, ctx);
};
// Check that @not_owner does not own the just-created ColorObject.
let not_owner = @0x2;
test_scenario::next_tx(scenario, not_owner);
{
assert!(!test_scenario::has_most_recent_for_sender<ColorObject>(scenario), 0);
};
// Check that @owner indeed owns the just-created ColorObject.
// Also checks the value fields of the object.
test_scenario::next_tx(scenario, owner);
{
let object = test_scenario::take_from_sender<ColorObject>(scenario);
let (red, green, blue) = color_object::get_color(&object);
assert!(red == 255 && green == 0 && blue == 255, 0);
test_scenario::return_to_sender(scenario, object);
};
test_scenario::end(scenario_val);
}
// == Tests covered in Chapter 2 ==
#[test]
fun test_copy_into() {
let owner = @0x1;
let scenario_val = test_scenario::begin(owner);
let scenario = &mut scenario_val;
// Create two ColorObjects owned by `owner`, and obtain their IDs.
let (id1, id2) = {
let ctx = test_scenario::ctx(scenario);
color_object::create(255, 255, 255, ctx);
let id1 =
object::id_from_address(tx_context::last_created_object_id(ctx));
color_object::create(0, 0, 0, ctx);
let id2 =
object::id_from_address(tx_context::last_created_object_id(ctx));
(id1, id2)
};
test_scenario::next_tx(scenario, owner);
{
let obj1 = test_scenario::take_from_sender_by_id<ColorObject>(scenario, id1);
let obj2 = test_scenario::take_from_sender_by_id<ColorObject>(scenario, id2);
let (red, green, blue) = color_object::get_color(&obj1);
assert!(red == 255 && green == 255 && blue == 255, 0);
color_object::copy_into(&obj2, &mut obj1);
test_scenario::return_to_sender(scenario, obj1);
test_scenario::return_to_sender(scenario, obj2);
};
test_scenario::next_tx(scenario, owner);
{
let obj1 = test_scenario::take_from_sender_by_id<ColorObject>(scenario, id1);
let (red, green, blue) = color_object::get_color(&obj1);
assert!(red == 0 && green == 0 && blue == 0, 0);
test_scenario::return_to_sender(scenario, obj1);
};
test_scenario::end(scenario_val);
}
#[test]
fun test_delete() {
let owner = @0x1;
// Create a ColorObject and transfer it to @owner.
let scenario_val = test_scenario::begin(owner);
let scenario = &mut scenario_val;
{
let ctx = test_scenario::ctx(scenario);
color_object::create(255, 0, 255, ctx);
};
// Delete the ColorObject we just created.
test_scenario::next_tx(scenario, owner);
{
let object = test_scenario::take_from_sender<ColorObject>(scenario);
color_object::delete(object);
};
// Verify that the object was indeed deleted.
test_scenario::next_tx(scenario, owner);
{
assert!(!test_scenario::has_most_recent_for_sender<ColorObject>(scenario), 0);
};
test_scenario::end(scenario_val);
}
#[test]
fun test_transfer() {
let owner = @0x1;
// Create a ColorObject and transfer it to @owner.
let scenario_val = test_scenario::begin(owner);
let scenario = &mut scenario_val;
{
let ctx = test_scenario::ctx(scenario);
color_object::create(255, 0, 255, ctx);
};
// Transfer the object to recipient.
let recipient = @0x2;
test_scenario::next_tx(scenario, owner);
{
let object = test_scenario::take_from_sender<ColorObject>(scenario);
color_object::transfer(object, recipient);
};
// Check that owner no longer owns the object.
test_scenario::next_tx(scenario, owner);
{
assert!(!test_scenario::has_most_recent_for_sender<ColorObject>(scenario), 0);
};
// Check that recipient now owns the object.
test_scenario::next_tx(scenario, recipient);
{
assert!(test_scenario::has_most_recent_for_sender<ColorObject>(scenario), 0);
};
test_scenario::end(scenario_val);
}
// == Tests covered in Chapter 3 ==
#[test]
fun test_immutable() {
let sender1 = @0x1;
let scenario_val = test_scenario::begin(sender1);
let scenario = &mut scenario_val;
{
let ctx = test_scenario::ctx(scenario);
color_object::create_immutable(255, 0, 255, ctx);
};
test_scenario::next_tx(scenario, sender1);
{
// take_owned does not work for immutable objects.
assert!(!test_scenario::has_most_recent_for_sender<ColorObject>(scenario), 0);
};
// Any sender can work.
let sender2 = @0x2;
test_scenario::next_tx(scenario, sender2);
{
let object_val = test_scenario::take_immutable<ColorObject>(scenario);
let object = &object_val;
let (red, green, blue) = color_object::get_color(object);
assert!(red == 255 && green == 0 && blue == 255, 0);
test_scenario::return_immutable(object_val);
};
test_scenario::end(scenario_val);
}
}
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
module tutorial::simple_warrior {
use std::option::{Self, Option};
use sui::object::{Self, UID};
use sui::transfer;
use sui::tx_context::{Self, TxContext};
struct Sword has key, store {
id: UID,
strength: u8,
}
struct Shield has key, store {
id: UID,
armor: u8,
}
struct SimpleWarrior has key {
id: UID,
sword: Option<Sword>,
shield: Option<Shield>,
}
public entry fun create_sword(strength: u8, ctx: &mut TxContext) {
let sword = Sword {
id: object::new(ctx),
strength,
};
transfer::transfer(sword, tx_context::sender(ctx))
}
public entry fun create_shield(armor: u8, ctx: &mut TxContext) {
let shield = Shield {
id: object::new(ctx),
armor,
};
transfer::transfer(shield, tx_context::sender(ctx))
}
public entry fun create_warrior(ctx: &mut TxContext) {
let warrior = SimpleWarrior {
id: object::new(ctx),
sword: option::none(),
shield: option::none(),
};
transfer::transfer(warrior, tx_context::sender(ctx))
}
public entry fun equip_sword(warrior: &mut SimpleWarrior, sword: Sword, ctx: &mut TxContext) {
if (option::is_some(&warrior.sword)) {
let old_sword = option::extract(&mut warrior.sword);
transfer::transfer(old_sword, tx_context::sender(ctx));
};
option::fill(&mut warrior.sword, sword);
}
public entry fun equip_shield(warrior: &mut SimpleWarrior, shield: Shield, ctx: &mut TxContext) {
if (option::is_some(&warrior.shield)) {
let old_shield = option::extract(&mut warrior.shield);
transfer::transfer(old_shield, tx_context::sender(ctx));
};
option::fill(&mut warrior.shield, shield);
}
}
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
module tutorial::trusted_swap {
use sui::balance::{Self, Balance};
use sui::coin::{Self, Coin};
use sui::object::{Self, UID};
use sui::sui::SUI;
use sui::transfer;
use sui::tx_context::{Self, TxContext};
const MIN_FEE: u64 = 1000;
struct Object has key, store {
id: UID,
scarcity: u8,
style: u8,
}
struct ObjectWrapper has key {
id: UID,
original_owner: address,
to_swap: Object,
fee: Balance<SUI>,
}
public entry fun create_object(scarcity: u8, style: u8, ctx: &mut TxContext) {
let object = Object {
id: object::new(ctx),
scarcity,
style,
};
transfer::public_transfer(object, tx_context::sender(ctx))
}
/// Anyone owns an `Object` can request swapping their object. This object
/// will be wrapped into `ObjectWrapper` and sent to `service_address`.
public entry fun request_swap(object: Object, fee: Coin<SUI>, service_address: address, ctx: &mut TxContext) {
assert!(coin::value(&fee) >= MIN_FEE, 0);
let wrapper = ObjectWrapper {
id: object::new(ctx),
original_owner: tx_context::sender(ctx),
to_swap: object,
fee: coin::into_balance(fee),
};
transfer::transfer(wrapper, service_address);
}
/// When the admin has two swap requests with objects that are trade-able,
/// the admin can execute the swap and send them back to the opposite owner.
public entry fun execute_swap(wrapper1: ObjectWrapper, wrapper2: ObjectWrapper, ctx: &mut TxContext) {
// Only swap if their scarcity is the same and style is different.
assert!(wrapper1.to_swap.scarcity == wrapper2.to_swap.scarcity, 0);
assert!(wrapper1.to_swap.style != wrapper2.to_swap.style, 0);
// Unpack both wrappers, cross send them to the other owner.
let ObjectWrapper {
id: id1,
original_owner: original_owner1,
to_swap: object1,
fee: fee1,
} = wrapper1;
let ObjectWrapper {
id: id2,
original_owner: original_owner2,
to_swap: object2,
fee: fee2,
} = wrapper2;
// Perform the swap.
transfer::transfer(object1, original_owner2);
transfer::transfer(object2, original_owner1);
// Service provider takes the fee.
let service_address = tx_context::sender(ctx);
balance::join(&mut fee1, fee2);
transfer::public_transfer(coin::from_balance(fee1, ctx), service_address);
// Effectively delete the wrapper objects.
object::delete(id1);
object::delete(id2);
}
}
// this line is added to create a gist. Empty file is not allowed.
// this line is added to create a gist. Empty file is not allowed.
[package]
name = "VecTest"
version = "0.0.1"
[dependencies]
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir= "crates/sui-framework/packages/sui-framework/", rev = "testnet" }
[addresses]
vector_test = "0x0"
module vector_test::person {
use std::string;
use sui::object::{Self, UID};
use sui::transfer;
use sui::tx_context::{Self, TxContext};
use std::string::utf8;
use std::vector;
struct Person has key {
id: UID,
name: string::String,
items: vector<string::String>
}
public entry fun create_person1(items: vector<string::String>, ctx: &mut TxContext) {
let person = Person {
id: object::new(ctx),
name: utf8(b"mike"),
items: items,
};
transfer::transfer(person, tx_context::sender(ctx));
}
public entry fun create_person2(items: vector<vector<u8>>, ctx: &mut TxContext) {
let inventory = vector::empty<string::String>();
while (vector::length(&items) > 0){
let item = vector::remove(&mut items, 0);
vector::push_back(&mut inventory, string::utf8(item));
};
let person = Person {
id: object::new(ctx),
name: utf8(b"mike"),
items: inventory,
};
transfer::transfer(person, tx_context::sender(ctx));
}
}
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
import "remix_tests.sol"; // this import is automatically injected by Remix.
import "hardhat/console.sol";
import "../contracts/3_Ballot.sol";
contract BallotTest {
bytes32[] proposalNames;
Ballot ballotToTest;
function beforeAll () public {
proposalNames.push(bytes32("candidate1"));
ballotToTest = new Ballot(proposalNames);
}
function checkWinningProposal () public {
console.log("Running checkWinningProposal");
ballotToTest.vote(0);
Assert.equal(ballotToTest.winningProposal(), uint(0), "proposal at index 0 should be the winning proposal");
Assert.equal(ballotToTest.winnerName(), bytes32("candidate1"), "candidate1 should be the winner name");
}
function checkWinninProposalWithReturnValue () public view returns (bool) {
return ballotToTest.winningProposal() == 0;
}
}
/* eslint-disable no-undef */
// Right click on the script name and hit "Run" to execute
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Storage", function () {
it("test initial value", async function () {
const Storage = await ethers.getContractFactory("Storage");
const storage = await Storage.deploy();
await storage.deployed();
console.log("storage deployed at:" + storage.address);
expect((await storage.retrieve()).toNumber()).to.equal(0);
});
it("test updating and retrieving updated value", async function () {
const Storage = await ethers.getContractFactory("Storage");
const storage = await Storage.deploy();
await storage.deployed();
const storage2 = await ethers.getContractAt("Storage", storage.address);
const setValue = await storage2.store(56);
await setValue.wait();
expect((await storage2.retrieve()).toNumber()).to.equal(56);
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment