suidouble 0.0.6 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +2 -0
- package/lib/SuiEvent.js +7 -0
- package/lib/SuiLocalTestValidator.js +5 -8
- package/lib/SuiMaster.js +21 -5
- package/lib/SuiMemoryObjectStorage.js +6 -0
- package/lib/SuiPackage.js +13 -3
- package/lib/SuiPackageModule.js +0 -2
- package/package.json +24 -4
- package/test/sui_master_basic.test.js +62 -0
- package/test/sui_master_onlocal.test.js +211 -0
- package/test/test_move_contracts/suidouble_chat/Move.lock +20 -0
- package/test/test_move_contracts/suidouble_chat/Move.toml +10 -0
- package/test/test_move_contracts/suidouble_chat/compiled.json +1 -0
- package/test/test_move_contracts/suidouble_chat/sources/suidouble_chat.move +221 -0
package/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
const SuiMaster = require('./lib/SuiMaster.js');
|
|
2
2
|
const SuiInBrowser = require('./lib/SuiInBrowser.js');
|
|
3
3
|
const SuiTestScenario = require('./lib/SuiTestScenario.js');
|
|
4
|
+
const SuiLocalTestValidator = require('./lib/SuiLocalTestValidator.js');
|
|
4
5
|
|
|
5
6
|
module.exports = {
|
|
6
7
|
SuiMaster,
|
|
7
8
|
SuiInBrowser,
|
|
8
9
|
SuiTestScenario,
|
|
10
|
+
SuiLocalTestValidator,
|
|
9
11
|
};
|
package/lib/SuiEvent.js
CHANGED
|
@@ -12,6 +12,13 @@ class SuiEvent extends SuiCommonMethods {
|
|
|
12
12
|
this._data = params.data || {};
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* In module type name, without package and module prefix
|
|
17
|
+
*/
|
|
18
|
+
get typeName() {
|
|
19
|
+
return this._data ? this._data.type.split('::').pop() : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
15
22
|
get data() {
|
|
16
23
|
return this._data;
|
|
17
24
|
}
|
|
@@ -10,6 +10,10 @@ class SuiLocalTestValidator extends SuiCommonMethods {
|
|
|
10
10
|
this._active = false;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
get active() {
|
|
14
|
+
return this._active;
|
|
15
|
+
}
|
|
16
|
+
|
|
13
17
|
static async launch(params = {}) {
|
|
14
18
|
if (SuiLocalTestValidator.__instance) {
|
|
15
19
|
return await SuiLocalTestValidator.__instance.launch();
|
|
@@ -27,19 +31,12 @@ class SuiLocalTestValidator extends SuiCommonMethods {
|
|
|
27
31
|
|
|
28
32
|
async launch() {
|
|
29
33
|
if (this._child && this._active) {
|
|
30
|
-
return
|
|
34
|
+
return this;
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
this.log('launching sui-test-validator ...');
|
|
34
38
|
|
|
35
39
|
this._child = await SuiCliCommands.spawn('sui-test-validator', { RUST_LOG: 'consensus=off' });
|
|
36
|
-
|
|
37
|
-
// spawn('sui-test-validator', [], {
|
|
38
|
-
// env: {
|
|
39
|
-
// ...process.env,
|
|
40
|
-
// RUST_LOG: 'consensus=off',
|
|
41
|
-
// }
|
|
42
|
-
// });
|
|
43
40
|
|
|
44
41
|
this.__readyLaunchedPromiseResolver = null;
|
|
45
42
|
this.__readyLaunchedPromise = new Promise((res)=>{
|
package/lib/SuiMaster.js
CHANGED
|
@@ -35,22 +35,34 @@ class SuiMaster extends SuiCommonMethods {
|
|
|
35
35
|
if (params.provider) {
|
|
36
36
|
if (params.provider == 'local' || (params.provider.constructor && params.provider.constructor.name && params.provider.constructor.name == 'SuiLocalTestValidator')) {
|
|
37
37
|
this._provider = new sui.JsonRpcProvider(sui.localnetConnection);
|
|
38
|
-
this._providerName = '
|
|
38
|
+
this._providerName = 'sui:localnet';
|
|
39
39
|
} else if (params.provider == 'test' || params.provider == 'testnet') {
|
|
40
40
|
this._provider = new sui.JsonRpcProvider(sui.testnetConnection);
|
|
41
|
-
this._providerName = '
|
|
41
|
+
this._providerName = 'sui:testnet';
|
|
42
42
|
} else if (params.provider == 'dev' || params.provider == 'devnet') {
|
|
43
43
|
this._provider = new sui.JsonRpcProvider(sui.devnetConnection);
|
|
44
|
-
this._providerName = '
|
|
44
|
+
this._providerName = 'sui:devnet';
|
|
45
45
|
} else if (params.provider == 'main' || params.provider == 'mainnet') {
|
|
46
46
|
this._provider = new sui.JsonRpcProvider(sui.mainnetConnection);
|
|
47
|
-
this._providerName = '
|
|
47
|
+
this._providerName = 'sui:mainnet';
|
|
48
48
|
|
|
49
49
|
this.log('we are on the mainnet, working with real money, be careful');
|
|
50
50
|
} else {
|
|
51
51
|
if (params.provider && params.provider.connection && params.provider.connection.fullnode) {
|
|
52
52
|
this._provider = params.provider;
|
|
53
|
-
|
|
53
|
+
|
|
54
|
+
if (params.provider.connection.fullnode.indexOf('devnet') !== -1) {
|
|
55
|
+
this._providerName = 'sui:devnet';
|
|
56
|
+
} else if (params.provider.connection.fullnode.indexOf('testnet') !== -1) {
|
|
57
|
+
this._providerName = 'sui:testnet';
|
|
58
|
+
} else if (params.provider.connection.fullnode.indexOf('mainnet') !== -1) {
|
|
59
|
+
this._providerName = 'sui:mainnet';
|
|
60
|
+
} else if (params.provider.connection.fullnode.indexOf('127.0.0.1') !== -1) {
|
|
61
|
+
this._providerName = 'sui:localnet';
|
|
62
|
+
} else {
|
|
63
|
+
// just keep provider name as unique to fullnode URL to keep separate ObjectStorage instances
|
|
64
|
+
this._providerName = params.provider.connection.fullnode;
|
|
65
|
+
}
|
|
54
66
|
}
|
|
55
67
|
}
|
|
56
68
|
}
|
|
@@ -85,6 +97,10 @@ class SuiMaster extends SuiCommonMethods {
|
|
|
85
97
|
return this._provider;
|
|
86
98
|
}
|
|
87
99
|
|
|
100
|
+
get connectedChain() {
|
|
101
|
+
return this._providerName;
|
|
102
|
+
}
|
|
103
|
+
|
|
88
104
|
get address() {
|
|
89
105
|
return this._address;
|
|
90
106
|
}
|
|
@@ -11,6 +11,12 @@ class SuiMemoryObjectStorage extends SuiCommonMethods {
|
|
|
11
11
|
return Object.values(this._objects);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
findMostRecentByTypeName(typeName) {
|
|
15
|
+
return this.findMostRecent((object) => {
|
|
16
|
+
return (object.typeName == typeName);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
find(filterFunction) {
|
|
15
21
|
for (const id in this._objects) {
|
|
16
22
|
if (filterFunction(this._objects[id])) {
|
package/lib/SuiPackage.js
CHANGED
|
@@ -43,6 +43,19 @@ class SuiPackage extends SuiObject {
|
|
|
43
43
|
return this._modules;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
async getModule(moduleName) {
|
|
47
|
+
await this.checkOnChainIfNeeded();
|
|
48
|
+
return this._modules[moduleName];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get isBuilt() {
|
|
52
|
+
return this._isBuilt;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get version() {
|
|
56
|
+
return Number(this._publishedVersion); // return as Number in getter
|
|
57
|
+
}
|
|
58
|
+
|
|
46
59
|
async isOnChain() {
|
|
47
60
|
try {
|
|
48
61
|
await this.checkOnChainIfNeeded();
|
|
@@ -223,9 +236,6 @@ class SuiPackage extends SuiObject {
|
|
|
223
236
|
},
|
|
224
237
|
});
|
|
225
238
|
|
|
226
|
-
console.log('result', result);
|
|
227
|
-
|
|
228
|
-
|
|
229
239
|
if (result?.data?.version) {
|
|
230
240
|
this._publishedVersion = BigInt(result?.data?.version); // not sure, but it's string in response, so let's convert it to bigint, who knows
|
|
231
241
|
this._isPublished = true;
|
package/lib/SuiPackageModule.js
CHANGED
|
@@ -104,8 +104,6 @@ class SuiPackageModule extends SuiCommonMethods {
|
|
|
104
104
|
const listMutated = [];
|
|
105
105
|
const listDeleted = [];
|
|
106
106
|
|
|
107
|
-
console.error('result', result);
|
|
108
|
-
|
|
109
107
|
for (const objectChange of result.objectChanges) {
|
|
110
108
|
if (objectChange.objectId) {
|
|
111
109
|
if (this.objectStorage.byAddress(objectChange.objectId)) {
|
package/package.json
CHANGED
|
@@ -1,16 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "suidouble",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "Set of provider, package and object classes for javascript representation of Sui Move smart contracts. Use same code for publishing, upgrading, integration testing, interaction with smart contracts and integration in browser web3 dapps",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "
|
|
7
|
+
"test": "tap -j=1 ./test/*.test.js",
|
|
8
|
+
"coverage": "tap -j=1 ./test/*.test.js"
|
|
8
9
|
},
|
|
9
|
-
"keywords": [
|
|
10
|
+
"keywords": [
|
|
11
|
+
"sui",
|
|
12
|
+
"sui move",
|
|
13
|
+
"move",
|
|
14
|
+
"smart contract",
|
|
15
|
+
"smart contracts",
|
|
16
|
+
"sui.js",
|
|
17
|
+
"web3",
|
|
18
|
+
"dapps",
|
|
19
|
+
"dapp"
|
|
20
|
+
],
|
|
10
21
|
"author": "Jeka Kiselyov <jeka911@gmail.com> (https://github.com/jeka-kiselyov)",
|
|
11
22
|
"license": "Apache-2.0",
|
|
12
23
|
"dependencies": {
|
|
13
24
|
"@mysten/sui.js": "^0.34.0",
|
|
14
25
|
"@wallet-standard/core": "^1.0.3"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"tap": "^16.3.4"
|
|
29
|
+
},
|
|
30
|
+
"tap": {
|
|
31
|
+
"branches": 90,
|
|
32
|
+
"lines": 90,
|
|
33
|
+
"functions": 90,
|
|
34
|
+
"statements": 90
|
|
15
35
|
}
|
|
16
|
-
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const t = require('tap');
|
|
4
|
+
const { test } = t;
|
|
5
|
+
|
|
6
|
+
const { SuiMaster } = require('..');
|
|
7
|
+
|
|
8
|
+
test('initialization', async t => {
|
|
9
|
+
t.plan(2);
|
|
10
|
+
|
|
11
|
+
t.ok(true);
|
|
12
|
+
t.equal(1, 1, 'Ready state is (==1)');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('pseudo-random keypairs generation works ok', async t => {
|
|
16
|
+
const suiMaster = new SuiMaster({provider: 'test', as: 'somebody'});
|
|
17
|
+
await suiMaster.initialize();
|
|
18
|
+
|
|
19
|
+
// pseudo-random generation of 'somebody' is a keypair for a wallet '0x15b9493fb639a3118fed766ca80c1da62fa20493c293f319cc7d136506d2db69'
|
|
20
|
+
// not sure if we need to assert it, as we may change pseudo-random generation algo, still keeping it function,
|
|
21
|
+
// so lets just check we make different keypairs depending on 'as' input parameter
|
|
22
|
+
// console.log(suiMaster.address);
|
|
23
|
+
|
|
24
|
+
t.ok(suiMaster.address); // there should be some address
|
|
25
|
+
t.ok(`${suiMaster.address}`.indexOf('0x') === 0); // adress is string starting with '0x'
|
|
26
|
+
|
|
27
|
+
const suiMasterAsAdmin = new SuiMaster({provider: 'test', as: 'admin'});
|
|
28
|
+
await suiMasterAsAdmin.initialize();
|
|
29
|
+
|
|
30
|
+
t.ok(suiMasterAsAdmin.address); // there should be some address
|
|
31
|
+
t.ok(`${suiMasterAsAdmin.address}`.indexOf('0x') === 0); // adress is string starting with '0x'
|
|
32
|
+
|
|
33
|
+
t.not(`${suiMaster.address}`, `${suiMasterAsAdmin.address}`, 'different pseudo randoms should be different');
|
|
34
|
+
|
|
35
|
+
/// but if you pass the same string as 'as' - it will generate the same keypair:
|
|
36
|
+
const suiMasterAsAdminAnother = new SuiMaster({provider: 'test', as: 'admin'});
|
|
37
|
+
await suiMasterAsAdminAnother.initialize();
|
|
38
|
+
|
|
39
|
+
t.equal(`${suiMasterAsAdminAnother.address}`, `${suiMasterAsAdmin.address}`, 'same string should generate same pseudo-random');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('connecting to different chains', async t => {
|
|
43
|
+
const suiMaster = new SuiMaster({provider: 'test', as: 'somebody'});
|
|
44
|
+
await suiMaster.initialize();
|
|
45
|
+
|
|
46
|
+
t.equal(suiMaster.connectedChain, 'sui:testnet');
|
|
47
|
+
|
|
48
|
+
const suiMaster2 = new SuiMaster({provider: 'dev', as: 'somebody'});
|
|
49
|
+
await suiMaster2.initialize();
|
|
50
|
+
|
|
51
|
+
t.equal(suiMaster2.connectedChain, 'sui:devnet');
|
|
52
|
+
|
|
53
|
+
const suiMaster3 = new SuiMaster({provider: 'main', as: 'somebody'});
|
|
54
|
+
await suiMaster3.initialize();
|
|
55
|
+
|
|
56
|
+
t.equal(suiMaster3.connectedChain, 'sui:mainnet');
|
|
57
|
+
|
|
58
|
+
const suiMaster4 = new SuiMaster({provider: 'local', as: 'somebody'});
|
|
59
|
+
await suiMaster4.initialize();
|
|
60
|
+
|
|
61
|
+
t.equal(suiMaster4.connectedChain, 'sui:localnet');
|
|
62
|
+
});
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const t = require('tap');
|
|
4
|
+
const { test } = t;
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const { SuiMaster, SuiLocalTestValidator } = require('..');
|
|
8
|
+
|
|
9
|
+
let suiLocalTestValidator = null;
|
|
10
|
+
let suiMaster = null;
|
|
11
|
+
let contract = null;
|
|
12
|
+
|
|
13
|
+
let contractAddressV1 = null;
|
|
14
|
+
let contractAddressV2 = null;
|
|
15
|
+
|
|
16
|
+
let chatShopObjectId = null;
|
|
17
|
+
|
|
18
|
+
test('spawn local test node', async t => {
|
|
19
|
+
suiLocalTestValidator = await SuiLocalTestValidator.launch();
|
|
20
|
+
t.ok(suiLocalTestValidator.active);
|
|
21
|
+
|
|
22
|
+
// SuiLocalTestValidator runs as signle instance. So you can't start it twice with static method
|
|
23
|
+
const suiLocalTestValidatorCopy = await SuiLocalTestValidator.launch();
|
|
24
|
+
t.equal(suiLocalTestValidator, suiLocalTestValidatorCopy);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('init suiMaster and connect it to local test validator', async t => {
|
|
28
|
+
suiMaster = new SuiMaster({provider: suiLocalTestValidator, as: 'somebody'});
|
|
29
|
+
await suiMaster.initialize();
|
|
30
|
+
|
|
31
|
+
t.ok(suiMaster.address); // there should be some address
|
|
32
|
+
t.ok(`${suiMaster.address}`.indexOf('0x') === 0); // adress is string starting with '0x'
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('request sui from faucet', async t => {
|
|
36
|
+
const balanceBefore = await suiMaster.getBalance();
|
|
37
|
+
await suiMaster.requestSuiFromFaucet();
|
|
38
|
+
|
|
39
|
+
const balanceAfter = await suiMaster.getBalance();
|
|
40
|
+
|
|
41
|
+
t.ok(balanceAfter > balanceBefore);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('attach a local package', async t => {
|
|
45
|
+
contract = suiMaster.addPackage({
|
|
46
|
+
path: path.join(__dirname, './test_move_contracts/suidouble_chat/'),
|
|
47
|
+
});
|
|
48
|
+
// there's nothing in contract yet, it's not built, not published (don't know it's id on chain)
|
|
49
|
+
//
|
|
50
|
+
// we can check that contract's objectStorage is the same instance as on master. Reminder, it's shared between all connections to same chain
|
|
51
|
+
t.equal(contract.objectStorage, suiMaster.objectStorage);
|
|
52
|
+
|
|
53
|
+
// lets try to build it
|
|
54
|
+
await contract.build();
|
|
55
|
+
|
|
56
|
+
t.ok(contract.isBuilt);
|
|
57
|
+
|
|
58
|
+
// and publish
|
|
59
|
+
|
|
60
|
+
await contract.publish();
|
|
61
|
+
|
|
62
|
+
t.ok(contract.address); // there should be some address
|
|
63
|
+
t.ok(contract.id); // same as id
|
|
64
|
+
t.ok(`${contract.address}`.indexOf('0x') === 0); // adress is string starting with '0x'
|
|
65
|
+
|
|
66
|
+
t.equal(contract.version, 1);
|
|
67
|
+
|
|
68
|
+
// there should be module 'suidouble_chat' on the contract we published
|
|
69
|
+
t.ok(contract.modules.suidouble_chat);
|
|
70
|
+
|
|
71
|
+
// we can check that contract's objectStorage is the same instance as on master. Reminder, it's shared between all connections to same chain
|
|
72
|
+
t.equal(contract.modules.suidouble_chat.objectStorage, suiMaster.objectStorage);
|
|
73
|
+
|
|
74
|
+
contractAddressV1 = contract.address;
|
|
75
|
+
|
|
76
|
+
// we'd need to .build() again after changes here. But it lets you upgrade package with very same code
|
|
77
|
+
await contract.upgrade();
|
|
78
|
+
|
|
79
|
+
t.not(contract.address, contractAddressV1);
|
|
80
|
+
t.equal(contract.version, 2);
|
|
81
|
+
|
|
82
|
+
contractAddressV2 = contract.address;
|
|
83
|
+
|
|
84
|
+
// let's quickly check it worked, there should be event ChatShopCreated created and we can fetch it from contract's module
|
|
85
|
+
const eventsResponse = await contract.modules.suidouble_chat.fetchEvents();
|
|
86
|
+
// response is an instance of SuiPaginatedResponse
|
|
87
|
+
let foundChatShopCreatedEvent = false;
|
|
88
|
+
for (const event of eventsResponse.data) {
|
|
89
|
+
if (event.typeName === 'ChatShopCreated') {
|
|
90
|
+
foundChatShopCreatedEvent = true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
t.ok(foundChatShopCreatedEvent);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('attach a package by address on the blockchain', async t => {
|
|
98
|
+
suiMaster = new SuiMaster({provider: suiLocalTestValidator, as: 'somebody'});
|
|
99
|
+
await suiMaster.initialize();
|
|
100
|
+
|
|
101
|
+
contract = await suiMaster.addPackage({
|
|
102
|
+
id: contractAddressV2,
|
|
103
|
+
});
|
|
104
|
+
const eventsResponse = await contract.fetchEvents('suidouble_chat');
|
|
105
|
+
|
|
106
|
+
let foundChatShopCreatedEvent = false;
|
|
107
|
+
for (const event of eventsResponse.data) {
|
|
108
|
+
if (event.typeName === 'ChatShopCreated') {
|
|
109
|
+
foundChatShopCreatedEvent = true;
|
|
110
|
+
chatShopObjectId = event.parsedJson.id;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// there should be ChatShopCreated event
|
|
115
|
+
t.ok(foundChatShopCreatedEvent);
|
|
116
|
+
|
|
117
|
+
// it should have id of ChatShop object
|
|
118
|
+
t.ok(chatShopObjectId);
|
|
119
|
+
|
|
120
|
+
// there should be module 'suidouble_chat' on the contract
|
|
121
|
+
t.ok(contract.modules.suidouble_chat);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('can find a package on the blockchain by expected module name (in owned)', async t => {
|
|
125
|
+
suiMaster = new SuiMaster({provider: suiLocalTestValidator, as: 'somebody'});
|
|
126
|
+
await suiMaster.initialize();
|
|
127
|
+
|
|
128
|
+
contract = await suiMaster.addPackage({
|
|
129
|
+
modules: ['suidouble_chat'],
|
|
130
|
+
});
|
|
131
|
+
const eventsResponse = await contract.fetchEvents('suidouble_chat');
|
|
132
|
+
|
|
133
|
+
let foundChatShopCreatedEvent = false;
|
|
134
|
+
for (const event of eventsResponse.data) {
|
|
135
|
+
if (event.typeName === 'ChatShopCreated') {
|
|
136
|
+
foundChatShopCreatedEvent = true;
|
|
137
|
+
chatShopObjectId = event.parsedJson.id;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// there should be ChatShopCreated event
|
|
142
|
+
t.ok(foundChatShopCreatedEvent);
|
|
143
|
+
// it should have id of ChatShop object
|
|
144
|
+
t.ok(chatShopObjectId);
|
|
145
|
+
|
|
146
|
+
// there should be module 'suidouble_chat' on the contract
|
|
147
|
+
t.ok(contract.modules.suidouble_chat);
|
|
148
|
+
|
|
149
|
+
// it should find most recent version of the package
|
|
150
|
+
t.equal(contract.version, 2);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('execute contract methods', async t => {
|
|
154
|
+
const moveCallResult = await contract.moveCall('suidouble_chat', 'post', [chatShopObjectId, 'the message', 'metadata']);
|
|
155
|
+
|
|
156
|
+
// there're at least some object created
|
|
157
|
+
t.ok(moveCallResult.created.length > 0);
|
|
158
|
+
|
|
159
|
+
// by suidouble_chat contract design, ChatTopMessage is an object representing a thread,
|
|
160
|
+
// it always has at least one ChatResponse (with text of the very first message in thread)
|
|
161
|
+
let foundChatTopMessage = null;
|
|
162
|
+
let foundChatResponse = null;
|
|
163
|
+
let foundText = null;
|
|
164
|
+
moveCallResult.created.forEach((obj)=>{
|
|
165
|
+
if (obj.typeName == 'ChatTopMessage') {
|
|
166
|
+
foundChatTopMessage = true;
|
|
167
|
+
}
|
|
168
|
+
if (obj.typeName == 'ChatResponse') {
|
|
169
|
+
foundChatResponse = true;
|
|
170
|
+
foundText = obj.fields.text;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
t.ok(foundChatTopMessage);
|
|
175
|
+
t.ok(foundChatResponse);
|
|
176
|
+
|
|
177
|
+
// messageTextAsBytes = [].slice.call(new TextEncoder().encode(messageText)); // regular array with utf data
|
|
178
|
+
// suidouble_chat contract store text a bytes (easier to work with unicode things), let's convert it back to js string
|
|
179
|
+
foundText = new TextDecoder().decode(new Uint8Array(foundText));
|
|
180
|
+
|
|
181
|
+
t.equal(foundText, 'the message');
|
|
182
|
+
|
|
183
|
+
// now lets post a reply to the thread
|
|
184
|
+
// we need a ChatTopMessage to pass to move's 'reply' function
|
|
185
|
+
// we can find it via ChatTopMessageCreated events or from previous method execution results
|
|
186
|
+
// find from local objectStorage (where previous results are stored)
|
|
187
|
+
const chatTopMessage = contract.objectStorage.findMostRecentByTypeName('ChatTopMessage');
|
|
188
|
+
t.ok(chatTopMessage);
|
|
189
|
+
|
|
190
|
+
const responseTextAsBytes = [].slice.call(new TextEncoder().encode('ขอบคุณครับ, 🇺🇦')); // regular array with utf data
|
|
191
|
+
const moveCallResult2 = await contract.moveCall('suidouble_chat', 'reply', [chatTopMessage.id, responseTextAsBytes, 'metadata']);
|
|
192
|
+
|
|
193
|
+
// there're at least some object created
|
|
194
|
+
t.ok(moveCallResult2.created.length > 0);
|
|
195
|
+
|
|
196
|
+
let responseText = null;
|
|
197
|
+
moveCallResult2.created.forEach((obj)=>{
|
|
198
|
+
if (obj.typeName == 'ChatResponse') {
|
|
199
|
+
responseText = obj.fields.text;
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
// messageTextAsBytes = [].slice.call(new TextEncoder().encode(messageText)); // regular array with utf data
|
|
203
|
+
// suidouble_chat contract store text a bytes (easier to work with unicode things), let's convert it back to js string
|
|
204
|
+
responseText = new TextDecoder().decode(new Uint8Array(responseText));
|
|
205
|
+
|
|
206
|
+
t.equal(responseText, 'ขอบคุณครับ, 🇺🇦');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('stops local test node', async t => {
|
|
210
|
+
SuiLocalTestValidator.stop();
|
|
211
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# @generated by Move, please check-in and do not edit manually.
|
|
2
|
+
|
|
3
|
+
[move]
|
|
4
|
+
version = 0
|
|
5
|
+
|
|
6
|
+
dependencies = [
|
|
7
|
+
{ name = "Sui" },
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
[[move.package]]
|
|
11
|
+
name = "MoveStdlib"
|
|
12
|
+
source = { git = "https://github.com/MystenLabs/sui.git", rev = "testnet", subdir = "crates/sui-framework/packages/move-stdlib" }
|
|
13
|
+
|
|
14
|
+
[[move.package]]
|
|
15
|
+
name = "Sui"
|
|
16
|
+
source = { git = "https://github.com/MystenLabs/sui.git", rev = "testnet", subdir = "crates/sui-framework/packages/sui-framework" }
|
|
17
|
+
|
|
18
|
+
dependencies = [
|
|
19
|
+
{ name = "MoveStdlib" },
|
|
20
|
+
]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "suidouble_chat"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
|
|
5
|
+
[dependencies]
|
|
6
|
+
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "testnet" }
|
|
7
|
+
|
|
8
|
+
[addresses]
|
|
9
|
+
suidouble_chat = "0x0"
|
|
10
|
+
sui = "0000000000000000000000000000000000000000000000000000000000000002"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"modules":["oRzrCwYAAAALAQAQAhAyA0JQBJIBGgWsAYcBB7MCtQMI6AVABqgGKArQBlEMoQfQAg3xCQYAJAEOARIBFAEaASMBKAEpAAUDAAAHAwAAAwMAAAEIAAAECAAABgwAAAIMAAEABAEAAQQIBwAECwQABQkCAAcKAgAAFwABAAAbAgEAAB4DAQABKwEOAQACDBcBAgcMAhUZGgEHAh0bHAIHDAMTDAEBAwQWFAoBCAQZAAQABCoJCgAGIgwBAQgGKAgBAQgHIAUGAAwHBwsDDQsPBxIHEwgPCBUEFgsVBRgGFgwdAQcICwAEBggECgIKAgcICwQHCAUKAgoCBwgLAQgJAQYICwEFAQgDAgkABQEGCAkBCAgBCAABCQABCAoBCwcBCQABCAQICAkICAUICAgGCAkIBQgJAQIBCAEBCAIBBgkAAQgFAgoCCAYDBwgJCQAJAQEKAgIGCAkJAAEBAgcICQkAAQkBAQgGB0JhbGFuY2UMQ2hhdE93bmVyQ2FwDENoYXRSZXNwb25zZRNDaGF0UmVzcG9uc2VDcmVhdGVkCENoYXRTaG9wD0NoYXRTaG9wQ3JlYXRlZA5DaGF0VG9wTWVzc2FnZRVDaGF0VG9wTWVzc2FnZUNyZWF0ZWQCSUQDU1VJCVR4Q29udGV4dANVSUQDYWRkBmF1dGhvcgdiYWxhbmNlDGNoYXRfc2hvcF9pZBNjaGF0X3RvcF9tZXNzYWdlX2lkFGNoYXRfdG9wX3Jlc3BvbnNlX2lkFGR5bmFtaWNfb2JqZWN0X2ZpZWxkBGVtaXQFZXZlbnQHZXhpc3RzXwJpZARpbml0CG1ldGFkYXRhA25ldwZvYmplY3QEcG9zdAVwcmljZQZyZW1vdmUFcmVwbHkPcmVzcG9uc2VzX2NvdW50BnNlbmRlcgVzZXFfbgxzaGFyZV9vYmplY3QDc3VpDnN1aWRvdWJsZV9jaGF0BHRleHQOdG9wX21lc3NhZ2VfaWQPdG9wX3Jlc3BvbnNlX2lkCHRyYW5zZmVyCnR4X2NvbnRleHQMdWlkX3RvX2lubmVyBHplcm8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAwgAAgAAAAAAAAMIAAAAAAAAAAAKAhEQYXNfY2hhdF9yZXNwb25zZQACARYICAECAhYICCcICAICAxYICCYICCEDAwIBFggJBAIDFggJHAMOCwcBCAoFAgUWCAkPCAgRCAgNBR8DBgIGFggJEAgIDQUlCgIYCgIhAwAAAAAEFAoAEQkSAwoALhENOAALABEJDAEOAREKEgA4AQsBBugDAAAAAAAAOAISBDgDAgEBBAAQRQ4BQREHACUEBgUMCwMBCwABBwEnCgMRCQwLCgMRCQwJDgsRCg4JEQoSATgEDgkRCg4LEQoGAAAAAAAAAAASAjgFCwsMBAsAOAYMBQoDLhENDAYOCREKDAcLBAsFCwcLBgYAAAAAAAAAABIFDAoLCQ4KOAcLAy4RDQsBCwIGAAAAAAAAAAASBgwIDQoPAAcCCwg4CAsKOAkCAgEEAARADgFBEQcAJQQGBQwLAwELAAEHAScKABAABwI4CgQZCgAPAAcCOAsKABABFDgMCgAQAhQGAQAAAAAAAAAWCgAPAhUKAxEJDAQOBBEKCgAQABEKCgAQAhQSAjgFCwQKAC44BwoDLhENCwELAgsAEAIUEgYLAy4RDTgMAgUABQMFBAA="],"dependencies":["0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000002"],"digest":[160,183,189,147,87,240,81,132,236,190,75,225,74,68,254,14,106,61,173,195,201,119,140,108,230,103,29,238,48,162,45,170]}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
|
|
2
|
+
module suidouble_chat::suidouble_chat {
|
|
3
|
+
use sui::object::{Self, UID, ID};
|
|
4
|
+
use sui::transfer;
|
|
5
|
+
use sui::tx_context::{Self, TxContext};
|
|
6
|
+
use std::vector::length;
|
|
7
|
+
|
|
8
|
+
use sui::dynamic_object_field::{Self};
|
|
9
|
+
|
|
10
|
+
use sui::sui::SUI;
|
|
11
|
+
use sui::balance::{Self, Balance};
|
|
12
|
+
|
|
13
|
+
use std::debug;
|
|
14
|
+
|
|
15
|
+
use sui::event::emit;
|
|
16
|
+
|
|
17
|
+
/// Max text length.
|
|
18
|
+
const MAX_TEXT_LENGTH: u64 = 512;
|
|
19
|
+
|
|
20
|
+
/// Text size overflow.
|
|
21
|
+
const ETextOverflow: u64 = 0;
|
|
22
|
+
|
|
23
|
+
// ======== Events =========
|
|
24
|
+
|
|
25
|
+
/// Event. When a new chat has been created.
|
|
26
|
+
struct ChatShopCreated has copy, drop { id: ID }
|
|
27
|
+
struct ChatTopMessageCreated has copy, drop { id: ID, top_response_id: ID }
|
|
28
|
+
struct ChatResponseCreated has copy, drop { id: ID, top_message_id: ID, seq_n: u64 }
|
|
29
|
+
|
|
30
|
+
/// Capability that grants an owner the right to collect profits.
|
|
31
|
+
struct ChatOwnerCap has key { id: UID }
|
|
32
|
+
|
|
33
|
+
/// A shared object. `key` ability is required.
|
|
34
|
+
struct ChatShop has key {
|
|
35
|
+
id: UID,
|
|
36
|
+
price: u64,
|
|
37
|
+
balance: Balance<SUI>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
struct ChatTopMessage has key, store {
|
|
41
|
+
id: UID,
|
|
42
|
+
chat_shop_id: ID,
|
|
43
|
+
chat_top_response_id: ID,
|
|
44
|
+
author: address,
|
|
45
|
+
responses_count: u64,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
struct ChatResponse has key, store {
|
|
49
|
+
id: UID,
|
|
50
|
+
chat_top_message_id: ID,
|
|
51
|
+
author: address,
|
|
52
|
+
text: vector<u8>,
|
|
53
|
+
// app-specific metadata. We do not enforce a metadata format and delegate this to app layer.
|
|
54
|
+
metadata: vector<u8>,
|
|
55
|
+
seq_n: u64, // n of message in thread
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Init function is often ideal place for initializing
|
|
59
|
+
/// a shared object as it is called only once.
|
|
60
|
+
///
|
|
61
|
+
/// To share an object `transfer::share_object` is used.
|
|
62
|
+
fun init(ctx: &mut TxContext) {
|
|
63
|
+
transfer::transfer(ChatOwnerCap {
|
|
64
|
+
id: object::new(ctx)
|
|
65
|
+
}, tx_context::sender(ctx));
|
|
66
|
+
|
|
67
|
+
let id = object::new(ctx);
|
|
68
|
+
emit(ChatShopCreated { id: object::uid_to_inner(&id) });
|
|
69
|
+
|
|
70
|
+
// Share the object to make it accessible to everyone!
|
|
71
|
+
transfer::share_object(ChatShop {
|
|
72
|
+
id: id,
|
|
73
|
+
price: 1000,
|
|
74
|
+
balance: balance::zero()
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// /// Simple ChatResponse.text getter.
|
|
79
|
+
// public fun text(chat_response: &ChatResponse): String {
|
|
80
|
+
// chat_response.text
|
|
81
|
+
// }
|
|
82
|
+
|
|
83
|
+
/// Mint (post) a chatMessage object without referencing another object.
|
|
84
|
+
public entry fun post(
|
|
85
|
+
chat_shop: &ChatShop,
|
|
86
|
+
text: vector<u8>,
|
|
87
|
+
metadata: vector<u8>,
|
|
88
|
+
ctx: &mut TxContext,
|
|
89
|
+
) {
|
|
90
|
+
assert!(length(&text) <= MAX_TEXT_LENGTH, ETextOverflow);
|
|
91
|
+
let id = object::new(ctx);
|
|
92
|
+
let chat_response_id = object::new(ctx);
|
|
93
|
+
|
|
94
|
+
emit(ChatTopMessageCreated { id: object::uid_to_inner(&id), top_response_id: object::uid_to_inner(&chat_response_id), });
|
|
95
|
+
emit(ChatResponseCreated { id: object::uid_to_inner(&chat_response_id), top_message_id: object::uid_to_inner(&id), seq_n: 0 });
|
|
96
|
+
|
|
97
|
+
let chat_top_message = ChatTopMessage {
|
|
98
|
+
id: id,
|
|
99
|
+
chat_shop_id: object::id(chat_shop),
|
|
100
|
+
author: tx_context::sender(ctx),
|
|
101
|
+
chat_top_response_id: object::uid_to_inner(&chat_response_id),
|
|
102
|
+
responses_count: 0,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
let chat_response = ChatResponse {
|
|
106
|
+
id: chat_response_id,
|
|
107
|
+
chat_top_message_id: object::id(&chat_top_message),
|
|
108
|
+
author: tx_context::sender(ctx),
|
|
109
|
+
text: text,
|
|
110
|
+
metadata,
|
|
111
|
+
seq_n: 0,
|
|
112
|
+
};
|
|
113
|
+
dynamic_object_field::add(&mut chat_top_message.id, b"as_chat_response", chat_response);
|
|
114
|
+
|
|
115
|
+
transfer::share_object(chat_top_message);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
public entry fun reply(
|
|
119
|
+
chat_top_message: &mut ChatTopMessage,
|
|
120
|
+
text: vector<u8>,
|
|
121
|
+
metadata: vector<u8>,
|
|
122
|
+
ctx: &mut TxContext,
|
|
123
|
+
) {
|
|
124
|
+
assert!(length(&text) <= MAX_TEXT_LENGTH, ETextOverflow);
|
|
125
|
+
|
|
126
|
+
let dynamic_field_exists = dynamic_object_field::exists_(&chat_top_message.id, b"as_chat_response");
|
|
127
|
+
if (dynamic_field_exists) {
|
|
128
|
+
let top_level_chat_response = dynamic_object_field::remove<vector<u8>, ChatResponse>(&mut chat_top_message.id, b"as_chat_response");
|
|
129
|
+
transfer::transfer(top_level_chat_response, chat_top_message.author);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
chat_top_message.responses_count = chat_top_message.responses_count + 1;
|
|
133
|
+
|
|
134
|
+
let id = object::new(ctx);
|
|
135
|
+
|
|
136
|
+
emit(ChatResponseCreated { id: object::uid_to_inner(&id), top_message_id: object::uid_to_inner(&chat_top_message.id), seq_n: chat_top_message.responses_count });
|
|
137
|
+
|
|
138
|
+
let chat_response = ChatResponse {
|
|
139
|
+
id: id,
|
|
140
|
+
chat_top_message_id: object::id(chat_top_message),
|
|
141
|
+
author: tx_context::sender(ctx),
|
|
142
|
+
text: text,
|
|
143
|
+
metadata,
|
|
144
|
+
seq_n: chat_top_message.responses_count,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
transfer::transfer(chat_response, tx_context::sender(ctx));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
#[test]
|
|
153
|
+
public fun test_module_init() {
|
|
154
|
+
use sui::test_scenario;
|
|
155
|
+
|
|
156
|
+
// Create test address representing game admin
|
|
157
|
+
let admin = @0xBABE;
|
|
158
|
+
let somebody = @0xFAFE;
|
|
159
|
+
let anybody = @0xFAAE;
|
|
160
|
+
// let player = @0x0;
|
|
161
|
+
|
|
162
|
+
// First transaction to emulate module initialization
|
|
163
|
+
let scenario_val = test_scenario::begin(admin);
|
|
164
|
+
let scenario = &mut scenario_val;
|
|
165
|
+
|
|
166
|
+
// Run the module initializers
|
|
167
|
+
test_scenario::next_tx(scenario, admin);
|
|
168
|
+
{
|
|
169
|
+
init(test_scenario::ctx(scenario));
|
|
170
|
+
|
|
171
|
+
};
|
|
172
|
+
// Run the module initializers
|
|
173
|
+
test_scenario::next_tx(scenario, somebody);
|
|
174
|
+
{
|
|
175
|
+
let chat_shop = test_scenario::take_shared<ChatShop>(scenario);
|
|
176
|
+
// let chat_shop_ref = &chat_shop;
|
|
177
|
+
debug::print(&chat_shop);
|
|
178
|
+
|
|
179
|
+
let chat_shop_ref = &chat_shop;
|
|
180
|
+
post(chat_shop_ref, b"test", b"metadata", test_scenario::ctx(scenario));
|
|
181
|
+
|
|
182
|
+
// post(chat_shop_ref, b"test", b"metadata", test_scenario::ctx(scenario));
|
|
183
|
+
|
|
184
|
+
test_scenario::return_shared(chat_shop);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
test_scenario::next_tx(scenario, anybody);
|
|
188
|
+
{
|
|
189
|
+
// let chat_top_message = test_scenario::take_from_sender<ChatTopMessage>(scenario);
|
|
190
|
+
let chat_top_message = test_scenario::take_shared<ChatTopMessage>(scenario);
|
|
191
|
+
// let chat_shop_ref = &chat_shop;
|
|
192
|
+
debug::print(&chat_top_message);
|
|
193
|
+
debug::print(&mut chat_top_message);
|
|
194
|
+
|
|
195
|
+
// let chat_top_message_ref = &chat_top_message;
|
|
196
|
+
reply(&mut chat_top_message, b"response", b"metadata", test_scenario::ctx(scenario));
|
|
197
|
+
|
|
198
|
+
// post(chat_shop_ref, b"test", b"metadata", test_scenario::ctx(scenario));
|
|
199
|
+
|
|
200
|
+
test_scenario::return_shared(chat_top_message);
|
|
201
|
+
// test_scenario::return_to_sender(scenario, chat_top_message);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
test_scenario::next_tx(scenario, anybody);
|
|
205
|
+
{
|
|
206
|
+
// let chat_top_message = test_scenario::take_from_sender<ChatTopMessage>(scenario);
|
|
207
|
+
let chat_top_message = test_scenario::take_shared<ChatTopMessage>(scenario);
|
|
208
|
+
// let chat_shop_ref = &chat_shop;
|
|
209
|
+
debug::print(&mut chat_top_message);
|
|
210
|
+
|
|
211
|
+
// let chat_top_message_ref = &chat_top_message;
|
|
212
|
+
reply(&mut chat_top_message, b"response", b"metadata", test_scenario::ctx(scenario));
|
|
213
|
+
|
|
214
|
+
// post(chat_shop_ref, b"test", b"metadata", test_scenario::ctx(scenario));
|
|
215
|
+
|
|
216
|
+
test_scenario::return_shared(chat_top_message);
|
|
217
|
+
// test_scenario::return_to_sender(scenario, chat_top_message);
|
|
218
|
+
};
|
|
219
|
+
test_scenario::end(scenario_val);
|
|
220
|
+
}
|
|
221
|
+
}
|