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%+
@@ -79,7 +79,13 @@ class SuiPackageModule extends SuiCommonMethods {
79
79
 
80
80
  const callArgs = [];
81
81
  for (let param of params) {
82
- callArgs.push(tx.pure(param));
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.11",
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 chatMessage object without referencing another object.
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) <= MAX_TEXT_LENGTH, ETextOverflow);
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);