suidouble
Set of provider, package and object classes for javascript representation of Sui's smart contracts. Use same code for publishing, upgrading, integration testing, interaction with smart contracts and integration in browser dapps. Very alpha for now.
Sample applications
installation
npm install suidouble --save
usage
connecting
Main class to interact with blockchain is SuiMaster:
const { SuiMaster } = require('suidouble');
You can initialize it directly, if you have keypair, secret phrase and can use it in code (so on node.js side - server side or CLI apps):
const suiMaster = new SuiMaster({
keypair: Ed25519Keypair,
debug: true,
provider: 'test',
});
const suiMaster = new SuiMaster({
debug: false,
phrase: 'thrive mean two thrive mean two thrive mean two thrive mean two',
provider: 'dev',
});
Also, there's option to generate pseudo-random phrases and wallets from strings, works like a charm for testing:
const suiMasterAsAdmin = new SuiMaster({ as: 'admin', provider: 'dev', });
const suiMasterAsUser = new SuiMaster({ as: 'user', provider: 'dev', });
On browser side, you'd probably want to use Sui wallets extensions adapters to sign message and don't store any keypairs or secret phrases in your code. So there's SuiInBrowser class for this, which can setup suiMaster instance for you. See 'Sui Move Connect in browser' section or sample UI application's code for more details.
const { SuiInBrowser } = require('suidouble');
const suiInBrowser = SuiInBrowser.getSingleton();
suiInBrowser.addEventListener('connected', async()=>{
const connectedSuiMaster = await suiInBrowser.getSuiMaster();
console.log('read-write on', suiInBrowser.getCurrentChain(), 'as', suiMaster.address);
});
suiInBrowser.connect(adapter);
Take a look at more detailed web3 connect code, sample application source code or check it online.
attaching a package
By default, suiMaster doesn't know of any smart contracts. There're 3 ways to attach one for interaction.
You can do it directly if you know contract's address (id). This is the option for browser apps and testing existing package:
const contract = suiMaster.addPackage({
id: '0x20cded4f9df05e37b44e3be2ffa9004dec77786950719fad6083694fdca45bf2',
});
await contract.isOnChain();
On node.js side, if you have Move's project with package code, you can attach it with path. This is the option for TDD and package publishing.
const contract = suiMaster.addPackage({
path: '../path_to_move_project_root/',
});
await contract.isOnChain();
Yes, it can find it's address on chain, by comparing Move's module names with package you own on chain. Works ok if you want to test upgrading or something. Also, you can attach the package only by modules names. This will work in browser too (note: you have to own this package, its UpgradeCap):
const contract = suiMaster.addPackage({
modules: ['chat', 'anothermodulename'],
});
await contract.isOnChain();
interacting with smart contract
SuiObject
Everyhing in Sui is an object. So is in suidouble. SuiObject's instance class follows:
suiObject.id;
suiObject.address;
suiObject.isShared;
suiObject.isImmutable;
suiObject.isDeleted;
suiObject.type;
suiObject.typeName;
suiObject.fields;
suiObject.display;
suiObject.localProperties;
suiObject.isOwnedBy('0x10cded4f9df05e37b44e3be2ffa9004dec77786950719fad6083694fdca45bf2');
@todo: better SuiObject documentation
fetching events
const events = await contract.fetchEvents('modulename', {eventTypeName: 'ChatResponseCreated', order: 'descending'});
while (events.hasNextPage) {
for (const event of events.data) {
console.log('event', event.parsedJson);
console.log('timestamp', event.timestampMs);
}
await events.nextPage();
}
subscribing to events
You can subscribe to Sui's contract events on package's module level. No types-etc filters for now ( @todo? )
const module = await contract.getModule('suidouble_chat');
await module.subscribeEvents();
module.addEventListener('ChatResponseCreated', (suiEvent)=>{
console.log(suiEvent.typeName);
console.log(suiEvent.parsedJson);
});
module.addEventListener('ChatTopMessageCreated', (suiEvent)=>{
console.log(suiEvent.typeName);
console.log(suiEvent.parsedJson);
});
Don't forget to unsubscribe from events when you don't need them anymore:
await module.unsubscribeEvents();
executing smart contract method
const res = await contract.moveCall('chat', 'post', ['0x10cded4f9df05e37b44e3be2ffa9004dec77786950719fad6083694fdca45bf2', [3,24,55], 'anotherparam']);
console.log(res);
for (const object of res.created) {
console.log('created', object.address, 'with type of', object.typeName);
}
for (const object of res.mutated) {
console.log('mutated', object.address, 'with type of', object.typeName);
}
for (const object of res.deleted) {
console.log('deleted', object.address, 'with type of', object.typeName, object.isDeleted);
}
If you need to transfer some SUI as part of executing contract method, you can use a magic parameter in form of {type: 'SUI', amount: 400000000000n} where 400000000000 is the amount of MIST you want to send. SuiPackageModule will convert this amount to Coin object using Transactions.SplitCoins method.
amount: 400000000000n
, amount: '400000000000'
, amount: 400000000000
will work too
const moveCallResult = await contract.moveCall('suidouble_chat', 'post_pay', [chatShopObjectId, {type: 'SUI', amount: 400000000000n}, messageText, 'metadata']);
@todo: sending other Coins
fetching objects
There's instance of SuiMemoryObjectStorage attached to every SuiMaster instance. Every smart contract method call adds created and mutated objects to it. You can also attach any object with it's address (id).
contract.modules.modulename.pushObject('0x10cded4f9df05e37b44e3be2ffa9004dec77786950719fad6083694fdca45bf2');
await contract.modules.modulename.fetchObjects();
const object = contract.modules.modulename.objectStorage.byAddress('0x10cded4f9df05e37b44e3be2ffa9004dec77786950719fad6083694fdca45bf2');
@todo: move pushing/fetching to SuiMemoryObjectStorage directly, as there's nothing package or module related?
@todo: invalidation? No need to re-fetch all objects each time
publishing the package
Builds a package and publish it to blockchain. CLI thing, as it needs execSync
to run sui move build
. Tested on Ubuntu, works good. If you have some issues with other platforms - please feel free to let me know or post Pull Request.
const { SuiMaster } = require('suidouble');
const provider = 'dev';
const suiMaster = new SuiMaster({ debug: true, as: 'admin', provider: provider, });
await suiMaster.requestSuiFromFaucet();
await suiMaster.getBalance();
const package = suiMaster.addPackage({
path: '../path_to_move_project_root/',
});
await package.publish();
console.log('published as', package.address);
upgrading the package
Same, it's for CLI as it re-builds the package.
const { SuiMaster } = require('suidouble');
const provider = 'local';
const suiMaster = new SuiMaster({ debug: true, as: 'admin', provider: provider, });
await suiMaster.requestSuiFromFaucet();
await suiMaster.getBalance();
const package = suiMaster.addPackage({
path: '../path_to_move_project_root/',
});
if (!(await package.isOnChain())) {
await package.publish();
} else {
await package.upgrade();
}
Sui Move Integration Testing
CLI integration tests, it runs local testing node (has to be installed), build and deploy a Move package into it and run unit tests over.
suidouble try to mimic Sui Move's testing framework:
const SuiTestScenario = require('./lib/SuiTestScenario.js');
const testScenario = new SuiTestScenario({
path: '../path_to_move_project_root/',
debug: true,
});
await testScenario.begin('admin');
await testScenario.init();
await testScenario.nextTx('admin', async()=>{
const chatShop = testScenario.takeShared('ChatShop');
await testScenario.moveCall('chat', 'post', [chatShop.address, 'posting a message', 'metadata']);
const chatTopMessage = testScenario.takeShared('ChatTopMessage');
assert(chatTopMessage != null);
assert(chatTopMessage.id != null);
});
await testScenario.nextTx('somebody', async()=>{
const chatTopMessage = testScenario.takeShared('ChatTopMessage');
await testScenario.moveCall('chat', 'reply', [chatTopMessage.address, 'posting a response', 'metadata']);
const chatResponse = testScenario.takeFromSender('ChatResponse');
assert(chatResponse != null);
assert(chatResponse.id != null);
});
await testScenario.end();
Sui Move Connect in browser
const { SuiInBrowser } = require('suidouble');
const suiInBrowser = SuiInBrowser.getSingleton();
const suiMaster = await suiInBrowser.getSuiMaster();
console.log('read-only on', suiInBrowser.getCurrentChain());
suiInBrowser.addEventListener('adapter', (adapter)=>{
console.log(adapter.name);
console.log(adapter.icon);
console.log(adapter.getDownloadURL());
if (adapter.name == 'Sui Wallet') {
suiInBrowser.connect(adapter);
}
});
suiInBrowser.addEventListener('connected', async()=>{
const connectedSuiMaster = await suiInBrowser.getSuiMaster();
console.log('read-write on', suiInBrowser.getCurrentChain(), 'as', suiMaster.address);
const contract = connectedSuiMaster.addPackage({
id: '0x20cded4f9df05e37b44e3be2ffa9004dec77786950719fad6083694fdca45bf2',
});
await contract.isOnChain();
const events = await contract.fetchEvents('chat', {eventTypeName: 'ChatResponseCreated', order: 'descending'});
for (const event of events.data) {
console.log('event', event.parsedJson);
}
const res = await contract.moveCall('chat', 'post', ['somedata', [3,24,55], 'anotherparam']);
console.log(res);
for (const object of res.created) {
console.log('created', object.address, 'with type of', object.typeName);
}
});
Unit tests
npm install
npm run tests
Take a look at unit tests code for some inspiration.
Todo
- subscribe to events
- sending other coins as contract methods execution
- suiobject invalidation/fetching optimization
- better documentation
- unit tests coverage to 90%+