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 || Secp256r1Keypair || Secp256k1Keypair,
debug: true,
client: 'test',
});
const suiMaster = new SuiMaster({
debug: false,
phrase: 'thrive mean two thrive mean two thrive mean two thrive mean two',
client: 'dev',
});
const suiMaster = new SuiMaster({
debug: false,
phrase: 'thrive mean two thrive mean two thrive mean two thrive mean two',
accountIndex: 1,
client: 'dev',
});
const suiMaster = new SuiMaster({
debug: false,
phrase: 'thrive mean two thrive mean two thrive mean two thrive mean two',
keypairAlgo: 'secp256k1',
client: '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', client: 'dev', });
const suiMasterAsUser = new SuiMaster({ as: 'user', client: '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');
await suiObject.getPastObject(version);
await suiObject.getPastObject();
await suiObject.queryTransactionBlocks();
@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
*** Subscribe to Events is deprecated in Sui SDK *** You should plan to use different architecture in your application.
You can subscribe to Sui's contract events on package's module level.
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', contract.arg('vector<u8>', [3,24,55]), contract.arg('string', '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);
}
move methods argumets types
Sui forces you to specify argument type in SDK v1.0, so we are going to follow this paradigm. With few little helpers. Both SuiPackage
and SuiPackageModule
have methods to make Inputs.Pure with bcs for you based on the desired type, you can use for executing suiPackage.moveCall
or suiPackageModule.moveCall
:
const arguments = [];
arguments.push(contract.arg('bool', true));
arguments.push(contract.arg('u8', 222));
arguments.push(contract.arg('u16', 2222));
arguments.push(contract.arg('u32', 3333));
arguments.push(contract.arg('u64', 4444));
arguments.push(contract.arg('u128', 5555));
arguments.push(contract.arg('u256', 6666));
arguments.push(contract.arg('address', '0xd9a95d7cc137f71dd7766f02791536453062a7509e9f461620cc4f583b09134c'));
arguments.push(contract.arg('string', 'some utf-8 💧string'));
arguments.push(contract.arg('vector<u8>', [222,111,211]));
Take a look at unit test covering all types arguments here
move methods typed arguments
To specify types for move methods declared as:
public entry fun method<T>(...)
you can specify typeArguments
as a 3rd parameter to suiPackageModule.moveCall
or 4th to suiPackage.moveCall
:
await mod.moveCall('test_method', [ store.id ], [ '0xca90beae66f23df1a830357c92e0a4348b6164d142c96b06936c5f28fdeaa99f::different_types::Store' ]);
await contract.moveCall('module_name', 'test_method', [ store.id ], [ '0xca90beae66f23df1a83036936c5f28fdeaa99f::different_types::Store' ]);
sending sui / coins with smart contract methods
If you need to transfer some SUI/coins as part of executing contract method, you can use a magic parameter in form of:
{type: 'SUI', amount: 400000000000n}
{type: 'SUI', amount: '0.2'}
{type: '0x5d4b302506645c37ff133b98c4b50a5ae14841659738d6d733d59d0d217a93bf::coin::COIN', amount: '1.0'}
{type: '0xc060006111016b8a020ad5b33834984a437aaa7d3c74c18e09a95d48aceab08c::coin::COIN', amount: '99.99'}
So executing
const params = [
chatShopObjectId,
{type: '0xc060006111016b8a020ad5b33834984a437aaa7d3c74c18e09a95d48aceab08c::coin::COIN', amount: '9.99'},
contract.arg('string', messageText),
];
const moveCallResult = await contract.moveCall('suidouble_chat', 'post_pay', params);
will send 9.99 USDT as the second parameter of the package method. Suidouble will convert needed coins using Sui's SplitCoins and MergeCoins internally to match amount you expect to send.
Some smart contracts requires clients to send coins in form of vectors. This is covered too, just pass magic parameter if the form of an array with one element:
const params = [
chatShopObjectId,
[{type: '0xc060006111016b8a020ad5b33834984a437aaa7d3c74c18e09a95d48aceab08c::coin::COIN', amount: '9.99'}],
contract.arg('string', messageText),
];
Don't forget to test transactions sending real money on devnet/testnet first!
composing transaction block yourself
If you need more flexebility, there's always an option to construct the transaction yourself:
const { Transaction, txInput } = require('suidobule');
const tx = new Transaction();
tx.moveCall({
target: `package_id::module_id::method_name`,
arguments: [
txInput(tx, 'u256', some_value),
txInput(tx, 'vector<bool>', some_array),
tx.object('0xc060006111016b8a020ad5b33834984a437aaa7d3c74c18e09a95d48aceab08c'),
],
});
const moveCallResult = await contract.moveCall('suidouble_chat', 'post_pay', {tx: tx});
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');
Another option (if you don't know the object id) is to query current wallet owned module's objects from blockchain:
const module = await contract.getModule('suidouble_chat');
const paginatedResponse = await module.getOwnedObjects();
const paginatedResponse2 = await module.getOwnedObjects({ typeName: 'ChatResponse' });
await paginatedResponse.forEach(async(suiObject)=>{
console.log(suiObject.id, suiObject.typeName, suiObject.fields);
}, maxLimit);
@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 client = 'dev';
const suiMaster = new SuiMaster({ debug: true, as: 'admin', client: client, });
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 client = 'local';
const suiMaster = new SuiMaster({ debug: true, as: 'admin', client: client, });
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, testScenario.arg('string', 'posting a message'), testScenario.arg('string', '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, testScenario.arg('string', 'posting a response'), testScenario.arg('string', 'metadata') ]);
const chatResponse = testScenario.takeFromSender('ChatResponse');
assert(chatResponse != null);
assert(chatResponse.id != null);
});
await testScenario.end();
Sui Move Connect in browser
Check out suidouble Vue component to connect your dapp to the Sui blockchain.
Or write the one manually, code is framework independed:
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', [contract.arg('string', 'somedata'), contract.arg('vector<u8>', 'somedata') ]);
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
- suiobject invalidation/fetching optimization
- better documentation
- unit tests coverage to 90%+