suidouble 0.0.11 → 0.0.13
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/README.md
CHANGED
|
@@ -155,6 +155,14 @@ const res = await contract.moveCall('chat', 'post', ['0x10cded4f9df05e37b44e3be2
|
|
|
155
155
|
}
|
|
156
156
|
```
|
|
157
157
|
|
|
158
|
+
If you need to transfer some SUI as part of executing contract method, you can use a magic parameter in form of '<SUI>400000000000' where 400000000000 is the amount of MIST you want to send. SuiPackageModule will convert this amount to Coin object using Transactions.SplitCoins method.
|
|
159
|
+
|
|
160
|
+
```javascript
|
|
161
|
+
const moveCallResult = await contract.moveCall('suidouble_chat', 'post_pay', [chatShopObjectId, '<SUI>400000000000', messageText, 'metadata']);
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
@todo: sending other Coins
|
|
165
|
+
|
|
158
166
|
##### fetching objects
|
|
159
167
|
|
|
160
168
|
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).
|
|
@@ -297,6 +305,7 @@ suiInBrowser.addEventListener('connected', async()=>{
|
|
|
297
305
|
### Todo
|
|
298
306
|
|
|
299
307
|
- subscribe to events
|
|
308
|
+
- sending other coins as contract methods execution
|
|
300
309
|
- suiobject invalidation/fetching optimization
|
|
301
310
|
- better documentation
|
|
302
|
-
- unit tests
|
|
311
|
+
- unit tests coverage to 90%+
|
package/lib/SuiPackageModule.js
CHANGED
|
@@ -79,7 +79,13 @@ class SuiPackageModule extends SuiCommonMethods {
|
|
|
79
79
|
|
|
80
80
|
const callArgs = [];
|
|
81
81
|
for (let param of params) {
|
|
82
|
-
|
|
82
|
+
if (param.indexOf && param.indexOf('<SUI>') === 0) {
|
|
83
|
+
let amount = BigInt(param.split('>')[1]);
|
|
84
|
+
const coin = tx.add(sui.Transactions.SplitCoins(tx.gas, [tx.pure(amount)]));
|
|
85
|
+
callArgs.push(coin);
|
|
86
|
+
} else {
|
|
87
|
+
callArgs.push(tx.pure(param));
|
|
88
|
+
}
|
|
83
89
|
}
|
|
84
90
|
tx.moveCall({
|
|
85
91
|
target: `${this._package.address}::${this._moduleName}::${methodName}`,
|
|
@@ -21,6 +21,24 @@ class SuiPaginatedResponse extends SuiCommonMethods {
|
|
|
21
21
|
this._data = [];
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Simple itterator to go over all list of items, not caring about pagination/cursors etc. It fetches next page when needed
|
|
26
|
+
* Optional maxLimit second parameter to stop when reached count
|
|
27
|
+
* @param {Function taking single argument of item} callbackFunc
|
|
28
|
+
* @param {Number} maxLimit
|
|
29
|
+
*/
|
|
30
|
+
async forEach(callbackFunc, maxLimit = null) {
|
|
31
|
+
let curN = 0;
|
|
32
|
+
do {
|
|
33
|
+
for (const item of this._data) {
|
|
34
|
+
if (!maxLimit || curN < maxLimit) {
|
|
35
|
+
await callbackFunc(item);
|
|
36
|
+
}
|
|
37
|
+
curN++;
|
|
38
|
+
}
|
|
39
|
+
} while( (!maxLimit || curN < maxLimit) && (await this.nextPage()) );
|
|
40
|
+
}
|
|
41
|
+
|
|
24
42
|
get hasNextPage() {
|
|
25
43
|
return this._hasNextPage;
|
|
26
44
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "suidouble",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.13",
|
|
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": {
|
|
@@ -211,6 +211,84 @@ test('execute contract methods', async t => {
|
|
|
211
211
|
t.equal(responseText, 'ขอบคุณครับ, 🇺🇦');
|
|
212
212
|
});
|
|
213
213
|
|
|
214
|
+
test('testing paginatedResponse', async t => {
|
|
215
|
+
const chatTopMessage = contract.objectStorage.findMostRecentByTypeName('ChatTopMessage');
|
|
216
|
+
t.ok(chatTopMessage);
|
|
217
|
+
|
|
218
|
+
// fill method create a lot of responses ( check out contract's code )
|
|
219
|
+
const moveCallResult = await contract.moveCall('suidouble_chat', 'fill', [chatTopMessage.id, 'the message response', 'metadata']);
|
|
220
|
+
t.ok(moveCallResult.created.length >= 60); // it's 60 in move code, but let's keep chat flexible
|
|
221
|
+
|
|
222
|
+
const eventsResponse = await contract.fetchEvents('suidouble_chat');
|
|
223
|
+
const idsInEventsDict = {};
|
|
224
|
+
let responsesInEventsCount = 0;
|
|
225
|
+
do {
|
|
226
|
+
for (const event of eventsResponse.data) {
|
|
227
|
+
if (!idsInEventsDict[event.parsedJson.id]) {
|
|
228
|
+
idsInEventsDict[event.parsedJson.id] = true;
|
|
229
|
+
responsesInEventsCount++;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} while(await eventsResponse.nextPage());
|
|
233
|
+
|
|
234
|
+
t.ok(responsesInEventsCount >= 60); // it's 60 in move code, but let's keep chat flexible
|
|
235
|
+
|
|
236
|
+
// or using SuiPaginatedResponse forEach itterator:
|
|
237
|
+
const anotherEventsResponse = await contract.fetchEvents('suidouble_chat');
|
|
238
|
+
let loopsInForEach = 0;
|
|
239
|
+
const idsInLoopDict = {};
|
|
240
|
+
await anotherEventsResponse.forEach(async (event)=>{ //
|
|
241
|
+
if (!idsInLoopDict[event.parsedJson.id]) {
|
|
242
|
+
idsInLoopDict[event.parsedJson.id] = true;
|
|
243
|
+
loopsInForEach++;
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
t.ok(loopsInForEach >= 60); // it's 60 in move code, but let's keep chat flexible
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('testing move call with coins', async t => {
|
|
251
|
+
const balanceWas = await suiMaster.getBalance();
|
|
252
|
+
|
|
253
|
+
const longMessageYouCanNotPostForFree = ('message ').padEnd(500, 'test');
|
|
254
|
+
// can't post it for free (as per contract design)
|
|
255
|
+
t.rejects(contract.moveCall('suidouble_chat', 'post', [chatShopObjectId, longMessageYouCanNotPostForFree, 'metadata']));
|
|
256
|
+
|
|
257
|
+
// but can post with with post_pay function sending some sui to it
|
|
258
|
+
const moveCallResult = await contract.moveCall('suidouble_chat', 'post_pay', [chatShopObjectId, '<SUI>400000000000', longMessageYouCanNotPostForFree, 'metadata']);
|
|
259
|
+
|
|
260
|
+
// there're at least some object created
|
|
261
|
+
t.ok(moveCallResult.created.length > 0);
|
|
262
|
+
|
|
263
|
+
// by suidouble_chat contract design, ChatTopMessage is an object representing a thread,
|
|
264
|
+
// it always has at least one ChatResponse (with text of the very first message in thread)
|
|
265
|
+
let foundChatTopMessage = null;
|
|
266
|
+
let foundChatResponse = null;
|
|
267
|
+
let foundText = null;
|
|
268
|
+
moveCallResult.created.forEach((obj)=>{
|
|
269
|
+
if (obj.typeName == 'ChatTopMessage') {
|
|
270
|
+
foundChatTopMessage = true;
|
|
271
|
+
}
|
|
272
|
+
if (obj.typeName == 'ChatResponse') {
|
|
273
|
+
foundChatResponse = true;
|
|
274
|
+
foundText = obj.fields.text;
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
t.ok(foundChatTopMessage);
|
|
279
|
+
t.ok(foundChatResponse);
|
|
280
|
+
|
|
281
|
+
// messageTextAsBytes = [].slice.call(new TextEncoder().encode(messageText)); // regular array with utf data
|
|
282
|
+
// suidouble_chat contract store text a bytes (easier to work with unicode things), let's convert it back to js string
|
|
283
|
+
foundText = new TextDecoder().decode(new Uint8Array(foundText));
|
|
284
|
+
|
|
285
|
+
t.equal(foundText, longMessageYouCanNotPostForFree);
|
|
286
|
+
|
|
287
|
+
const balanceNow = await suiMaster.getBalance();
|
|
288
|
+
|
|
289
|
+
t.ok( balanceNow <= (balanceWas - 400000000000n) );
|
|
290
|
+
});
|
|
291
|
+
|
|
214
292
|
test('stops local test node', async t => {
|
|
215
293
|
SuiLocalTestValidator.stop();
|
|
216
294
|
});
|
|
@@ -8,6 +8,7 @@ module suidouble_chat::suidouble_chat {
|
|
|
8
8
|
use sui::dynamic_object_field::{Self};
|
|
9
9
|
|
|
10
10
|
use sui::sui::SUI;
|
|
11
|
+
use sui::coin::{Self, Coin};
|
|
11
12
|
use sui::balance::{Self, Balance};
|
|
12
13
|
|
|
13
14
|
use std::debug;
|
|
@@ -15,11 +16,21 @@ module suidouble_chat::suidouble_chat {
|
|
|
15
16
|
use sui::event::emit;
|
|
16
17
|
|
|
17
18
|
/// Max text length.
|
|
19
|
+
const MAX_TEXT_LENGTH_FREE: u64 = 256;
|
|
18
20
|
const MAX_TEXT_LENGTH: u64 = 512;
|
|
19
21
|
|
|
22
|
+
/// Min price for a long message
|
|
23
|
+
const MIN_PRICE: u64 = 100;
|
|
24
|
+
|
|
20
25
|
/// Text size overflow.
|
|
21
26
|
const ETextOverflow: u64 = 0;
|
|
22
27
|
|
|
28
|
+
/// Not enough funds to pay for the good in question
|
|
29
|
+
const EInsufficientFunds: u64 = 1;
|
|
30
|
+
|
|
31
|
+
/// For when supplied Coin is zero.
|
|
32
|
+
const EZeroAmount: u64 = 2;
|
|
33
|
+
|
|
23
34
|
// ======== Events =========
|
|
24
35
|
|
|
25
36
|
/// Event. When a new chat has been created.
|
|
@@ -80,14 +91,56 @@ module suidouble_chat::suidouble_chat {
|
|
|
80
91
|
// chat_response.text
|
|
81
92
|
// }
|
|
82
93
|
|
|
83
|
-
/// Mint (post) a
|
|
94
|
+
/// Mint (post) a ChatTopMessage object without referencing another object.
|
|
84
95
|
public entry fun post(
|
|
85
96
|
chat_shop: &ChatShop,
|
|
86
97
|
text: vector<u8>,
|
|
87
98
|
metadata: vector<u8>,
|
|
88
99
|
ctx: &mut TxContext,
|
|
100
|
+
) {
|
|
101
|
+
assert!(length(&text) <= MAX_TEXT_LENGTH_FREE, ETextOverflow);
|
|
102
|
+
let id = object::new(ctx);
|
|
103
|
+
let chat_response_id = object::new(ctx);
|
|
104
|
+
|
|
105
|
+
emit(ChatTopMessageCreated { id: object::uid_to_inner(&id), top_response_id: object::uid_to_inner(&chat_response_id), });
|
|
106
|
+
emit(ChatResponseCreated { id: object::uid_to_inner(&chat_response_id), top_message_id: object::uid_to_inner(&id), seq_n: 0 });
|
|
107
|
+
|
|
108
|
+
let chat_top_message = ChatTopMessage {
|
|
109
|
+
id: id,
|
|
110
|
+
chat_shop_id: object::id(chat_shop),
|
|
111
|
+
author: tx_context::sender(ctx),
|
|
112
|
+
chat_top_response_id: object::uid_to_inner(&chat_response_id),
|
|
113
|
+
responses_count: 0,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
let chat_response = ChatResponse {
|
|
117
|
+
id: chat_response_id,
|
|
118
|
+
chat_top_message_id: object::id(&chat_top_message),
|
|
119
|
+
author: tx_context::sender(ctx),
|
|
120
|
+
text: text,
|
|
121
|
+
metadata,
|
|
122
|
+
seq_n: 0,
|
|
123
|
+
};
|
|
124
|
+
dynamic_object_field::add(&mut chat_top_message.id, b"as_chat_response", chat_response);
|
|
125
|
+
|
|
126
|
+
transfer::share_object(chat_top_message);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// Mint (post) a ChatTopMessage object without referencing another object.
|
|
130
|
+
public entry fun post_pay(
|
|
131
|
+
chat_shop: &mut ChatShop,
|
|
132
|
+
sui: Coin<SUI>,
|
|
133
|
+
text: vector<u8>,
|
|
134
|
+
metadata: vector<u8>,
|
|
135
|
+
ctx: &mut TxContext,
|
|
89
136
|
) {
|
|
90
137
|
assert!(length(&text) <= MAX_TEXT_LENGTH, ETextOverflow);
|
|
138
|
+
assert!(coin::value(&sui) > 0, EZeroAmount);
|
|
139
|
+
assert!(coin::value(&sui) >= MIN_PRICE, EInsufficientFunds);
|
|
140
|
+
|
|
141
|
+
let sui_balance = coin::into_balance(sui);
|
|
142
|
+
balance::join(&mut chat_shop.balance, sui_balance);
|
|
143
|
+
|
|
91
144
|
let id = object::new(ctx);
|
|
92
145
|
let chat_response_id = object::new(ctx);
|
|
93
146
|
|
|
@@ -115,13 +168,15 @@ module suidouble_chat::suidouble_chat {
|
|
|
115
168
|
transfer::share_object(chat_top_message);
|
|
116
169
|
}
|
|
117
170
|
|
|
171
|
+
|
|
172
|
+
/// Mint (post) a ChatResponse object
|
|
118
173
|
public entry fun reply(
|
|
119
174
|
chat_top_message: &mut ChatTopMessage,
|
|
120
175
|
text: vector<u8>,
|
|
121
176
|
metadata: vector<u8>,
|
|
122
177
|
ctx: &mut TxContext,
|
|
123
178
|
) {
|
|
124
|
-
assert!(length(&text) <=
|
|
179
|
+
assert!(length(&text) <= MAX_TEXT_LENGTH_FREE, ETextOverflow);
|
|
125
180
|
|
|
126
181
|
let dynamic_field_exists = dynamic_object_field::exists_(&chat_top_message.id, b"as_chat_response");
|
|
127
182
|
if (dynamic_field_exists) {
|
|
@@ -148,6 +203,79 @@ module suidouble_chat::suidouble_chat {
|
|
|
148
203
|
}
|
|
149
204
|
|
|
150
205
|
|
|
206
|
+
// /// Mint (post) a ChatResponse object
|
|
207
|
+
// public entry fun reply_pay(
|
|
208
|
+
// chat_top_message: &mut ChatTopMessage,
|
|
209
|
+
// sui: Coin<SUI>,
|
|
210
|
+
// text: vector<u8>,
|
|
211
|
+
// metadata: vector<u8>,
|
|
212
|
+
// ctx: &mut TxContext,
|
|
213
|
+
// ) acquires ChatShop {
|
|
214
|
+
// assert!(length(&text) <= MAX_TEXT_LENGTH, ETextOverflow);
|
|
215
|
+
|
|
216
|
+
// let dynamic_field_exists = dynamic_object_field::exists_(&chat_top_message.id, b"as_chat_response");
|
|
217
|
+
// if (dynamic_field_exists) {
|
|
218
|
+
// let top_level_chat_response = dynamic_object_field::remove<vector<u8>, ChatResponse>(&mut chat_top_message.id, b"as_chat_response");
|
|
219
|
+
// transfer::transfer(top_level_chat_response, chat_top_message.author);
|
|
220
|
+
// };
|
|
221
|
+
|
|
222
|
+
// chat_top_message.responses_count = chat_top_message.responses_count + 1;
|
|
223
|
+
|
|
224
|
+
// let chat_shop = borrow_global_mut<ChatShop>(id_to_address(&chat_top_message.chat_shop_id));
|
|
225
|
+
// let sui_balance = coin::into_balance(sui);
|
|
226
|
+
// balance::join(&mut chat_shop.balance, sui_balance);
|
|
227
|
+
|
|
228
|
+
// let id = object::new(ctx);
|
|
229
|
+
|
|
230
|
+
// 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 });
|
|
231
|
+
|
|
232
|
+
// let chat_response = ChatResponse {
|
|
233
|
+
// id: id,
|
|
234
|
+
// chat_top_message_id: object::id(chat_top_message),
|
|
235
|
+
// author: tx_context::sender(ctx),
|
|
236
|
+
// text: text,
|
|
237
|
+
// metadata,
|
|
238
|
+
// seq_n: chat_top_message.responses_count,
|
|
239
|
+
// };
|
|
240
|
+
|
|
241
|
+
// transfer::transfer(chat_response, tx_context::sender(ctx));
|
|
242
|
+
// }
|
|
243
|
+
|
|
244
|
+
/// Mint a lot of responses (we need this to unit test SuiPaginatedResponse faster)
|
|
245
|
+
public entry fun fill(
|
|
246
|
+
chat_top_message: &mut ChatTopMessage,
|
|
247
|
+
text: vector<u8>,
|
|
248
|
+
metadata: vector<u8>,
|
|
249
|
+
ctx: &mut TxContext,
|
|
250
|
+
) {
|
|
251
|
+
assert!(length(&text) <= MAX_TEXT_LENGTH, ETextOverflow);
|
|
252
|
+
|
|
253
|
+
let dynamic_field_exists = dynamic_object_field::exists_(&chat_top_message.id, b"as_chat_response");
|
|
254
|
+
if (dynamic_field_exists) {
|
|
255
|
+
let top_level_chat_response = dynamic_object_field::remove<vector<u8>, ChatResponse>(&mut chat_top_message.id, b"as_chat_response");
|
|
256
|
+
transfer::transfer(top_level_chat_response, chat_top_message.author);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
let i: u64 = 0;
|
|
260
|
+
|
|
261
|
+
while (i < 60) {
|
|
262
|
+
let id = object::new(ctx);
|
|
263
|
+
|
|
264
|
+
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 });
|
|
265
|
+
|
|
266
|
+
let chat_response = ChatResponse {
|
|
267
|
+
id: id,
|
|
268
|
+
chat_top_message_id: object::id(chat_top_message),
|
|
269
|
+
author: tx_context::sender(ctx),
|
|
270
|
+
text: text,
|
|
271
|
+
metadata,
|
|
272
|
+
seq_n: chat_top_message.responses_count,
|
|
273
|
+
};
|
|
274
|
+
transfer::transfer(chat_response, tx_context::sender(ctx));
|
|
275
|
+
i = i + 1;
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
151
279
|
|
|
152
280
|
#[test]
|
|
153
281
|
public fun test_module_init() {
|
|
@@ -201,6 +329,22 @@ module suidouble_chat::suidouble_chat {
|
|
|
201
329
|
// test_scenario::return_to_sender(scenario, chat_top_message);
|
|
202
330
|
};
|
|
203
331
|
|
|
332
|
+
test_scenario::next_tx(scenario, anybody);
|
|
333
|
+
{
|
|
334
|
+
let chat_shop = test_scenario::take_shared<ChatShop>(scenario);
|
|
335
|
+
debug::print(&chat_shop);
|
|
336
|
+
|
|
337
|
+
// let chat_shop_ref = &chat_shop;
|
|
338
|
+
let ctx = test_scenario::ctx(scenario);
|
|
339
|
+
post_pay(&mut chat_shop, coin::mint_for_testing<SUI>(100, ctx), b"test", b"metadata", test_scenario::ctx(scenario));
|
|
340
|
+
|
|
341
|
+
// post(chat_shop_ref, b"test", b"metadata", test_scenario::ctx(scenario));
|
|
342
|
+
|
|
343
|
+
debug::print(&chat_shop);
|
|
344
|
+
|
|
345
|
+
test_scenario::return_shared(chat_shop);
|
|
346
|
+
};
|
|
347
|
+
|
|
204
348
|
test_scenario::next_tx(scenario, anybody);
|
|
205
349
|
{
|
|
206
350
|
// let chat_top_message = test_scenario::take_from_sender<ChatTopMessage>(scenario);
|