Skip to content

Commit cc5da05

Browse files
committed
added sc testing tool
1 parent 8cf67e6 commit cc5da05

37 files changed

+1859
-114
lines changed

src/Qubic.Bob/BobClient.cs

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public sealed class BobClient : IDisposable
1515
private readonly string _rpcEndpoint;
1616
private readonly JsonSerializerOptions _jsonOptions;
1717
private int _requestId;
18+
private int _scQueryNonce = (int)(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() & 0x7FFFFFFF);
1819

1920
/// <summary>
2021
/// Creates a new BobClient with the specified base URL.
@@ -215,10 +216,45 @@ public Task<List<BobLogEntry>> GetLogsAsync(
215216
}
216217

217218
/// <summary>
218-
/// Queries a smart contract's state.
219+
/// Queries a smart contract function. Bob uses an async query model:
220+
/// the first call enqueues the query, subsequent calls with the same nonce retrieve the result.
219221
/// </summary>
220-
public Task<string> QuerySmartContractAsync(int contractIndex, string inputData, CancellationToken cancellationToken = default)
221-
=> CallAsync<string>("qubic_querySmartContract", new object[] { contractIndex, inputData }, cancellationToken);
222+
/// <param name="contractIndex">Smart contract index (1-based).</param>
223+
/// <param name="funcNumber">Function number (inputType) to call.</param>
224+
/// <param name="hexData">Hex-encoded input data (without 0x prefix).</param>
225+
/// <param name="cancellationToken">Cancellation token.</param>
226+
/// <returns>Hex-encoded output data.</returns>
227+
public async Task<string> QuerySmartContractAsync(int contractIndex, int funcNumber, string hexData, CancellationToken cancellationToken = default)
228+
{
229+
var nonce = Interlocked.Increment(ref _scQueryNonce);
230+
var parameters = new object[]
231+
{
232+
new Dictionary<string, object>
233+
{
234+
["nonce"] = nonce,
235+
["scIndex"] = contractIndex,
236+
["funcNumber"] = funcNumber,
237+
["data"] = hexData
238+
}
239+
};
240+
241+
// Poll for up to ~5 seconds (50 attempts x 100ms)
242+
for (int i = 0; i < 50; i++)
243+
{
244+
var result = await CallAsync<ScQueryResponse>("qubic_querySmartContract", parameters, cancellationToken);
245+
246+
if (result.Error is { } error)
247+
throw new BobRpcException(-1, error);
248+
249+
if (result.Data != null)
250+
return result.Data;
251+
252+
// Pending - wait and retry with same nonce
253+
await Task.Delay(100, cancellationToken);
254+
}
255+
256+
throw new BobRpcException(-1, "Smart contract query timed out");
257+
}
222258

223259
#endregion
224260

src/Qubic.Bob/BobWebSocketClient.cs

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public sealed class BobWebSocketClient : IAsyncDisposable, IDisposable
2929
private Task? _receiveLoop;
3030
private Task? _healthCheckLoop;
3131
private int _requestId;
32+
private int _scQueryNonce = (int)(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() & 0x7FFFFFFF);
3233
private int _reconnectAttempts;
3334
private bool _disposed;
3435

@@ -846,12 +847,40 @@ public Task<List<BobLogEntry>> GetLogsAsync(
846847
return CallAsync<List<BobLogEntry>>("qubic_getLogs", parameters.ToArray(), cancellationToken);
847848
}
848849

849-
/// <summary>Queries a smart contract's state.</summary>
850-
public Task<string> QuerySmartContractAsync(
850+
/// <summary>Queries a smart contract function (async with polling).</summary>
851+
public async Task<string> QuerySmartContractAsync(
851852
int contractIndex,
852-
string inputData,
853+
int funcNumber,
854+
string hexData,
853855
CancellationToken cancellationToken = default)
854-
=> CallAsync<string>("qubic_querySmartContract", new object[] { contractIndex, inputData }, cancellationToken);
856+
{
857+
var nonce = Interlocked.Increment(ref _scQueryNonce);
858+
var parameters = new object[]
859+
{
860+
new Dictionary<string, object>
861+
{
862+
["nonce"] = nonce,
863+
["scIndex"] = contractIndex,
864+
["funcNumber"] = funcNumber,
865+
["data"] = hexData
866+
}
867+
};
868+
869+
for (int i = 0; i < 50; i++)
870+
{
871+
var result = await CallAsync<ScQueryResponse>("qubic_querySmartContract", parameters, cancellationToken);
872+
873+
if (result.Error is { } error)
874+
throw new BobRpcException(-1, error);
875+
876+
if (result.Data != null)
877+
return result.Data;
878+
879+
await Task.Delay(100, cancellationToken);
880+
}
881+
882+
throw new BobRpcException(-1, "Smart contract query timed out");
883+
}
855884

856885
#endregion
857886

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Qubic.Bob.Models;
4+
5+
internal sealed class ScQueryResponse
6+
{
7+
[JsonPropertyName("nonce")]
8+
public int Nonce { get; set; }
9+
10+
[JsonPropertyName("data")]
11+
public string? Data { get; set; }
12+
13+
[JsonPropertyName("pending")]
14+
public bool? Pending { get; set; }
15+
16+
[JsonPropertyName("error")]
17+
public string? Error { get; set; }
18+
19+
[JsonPropertyName("message")]
20+
public string? Message { get; set; }
21+
}

src/Qubic.Core/Contracts/Generated/Ccf.g.cs

Lines changed: 109 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public static class Procedures
5252
/// <summary>Input for query.</summary>
5353
public readonly struct GetProposalIndicesInput : ISmartContractInput
5454
{
55-
public const int Size = 5;
55+
public const int Size = 8;
5656

5757
public int SerializedSize => Size;
5858

@@ -63,7 +63,7 @@ public byte[] ToBytes()
6363
{
6464
var bytes = new byte[Size];
6565
bytes.AsSpan(0, 1)[0] = (byte)(ActiveProposals ? 1 : 0);
66-
BinaryPrimitives.WriteInt32LittleEndian(bytes.AsSpan(1), PrevProposalIndex);
66+
BinaryPrimitives.WriteInt32LittleEndian(bytes.AsSpan(4), PrevProposalIndex);
6767
return bytes;
6868
}
6969
}
@@ -94,7 +94,7 @@ public static GetProposalIndicesOutput FromBytes(ReadOnlySpan<byte> data)
9494
/// <summary>Input for query.</summary>
9595
public readonly struct GetProposalInput : ISmartContractInput
9696
{
97-
public const int Size = 34;
97+
public const int Size = 40;
9898

9999
public int SerializedSize => Size;
100100

@@ -155,7 +155,7 @@ public static GetProposalOutput FromBytes(ReadOnlySpan<byte> data)
155155
/// <summary>Input for query.</summary>
156156
public readonly struct GetVoteInput : ISmartContractInput
157157
{
158-
public const int Size = 34;
158+
public const int Size = 40;
159159

160160
public int SerializedSize => Size;
161161

@@ -224,17 +224,59 @@ public static GetVotingResultsOutput FromBytes(ReadOnlySpan<byte> data)
224224

225225
// ═══ Function: GetLatestTransfers (inputType=5) ═══
226226

227+
/// <summary>Nested type from GetLatestTransfers.</summary>
228+
public readonly struct GetLatestTransfersLatestTransfersEntry
229+
{
230+
public const int Size = 304;
231+
232+
public required byte[] Destination { get; init; }
233+
public byte[] Url { get; init; }
234+
public long Amount { get; init; }
235+
public uint Tick { get; init; }
236+
public bool Success { get; init; }
237+
238+
public static GetLatestTransfersLatestTransfersEntry ReadFrom(ReadOnlySpan<byte> data)
239+
{
240+
var url = new byte[256];
241+
for (int i = 0; i < 256; i++)
242+
{
243+
url[i] = data.Slice(32 + i * 1, 1)[0];
244+
}
245+
return new GetLatestTransfersLatestTransfersEntry
246+
{
247+
Destination = data[0..].Slice(0, 32).ToArray(),
248+
Url = url,
249+
Amount = BinaryPrimitives.ReadInt64LittleEndian(data[288..]),
250+
Tick = BinaryPrimitives.ReadUInt32LittleEndian(data[296..]),
251+
Success = (data.Slice(300, 1)[0] != 0)
252+
};
253+
}
254+
}
255+
227256
/// <summary>Input for query (empty).</summary>
228257
public readonly struct GetLatestTransfersInput : ISmartContractInput
229258
{
230259
public int SerializedSize => 0;
231260
public byte[] ToBytes() => [];
232261
}
233262

234-
/// <summary>Output (empty).</summary>
263+
/// <summary>Output.</summary>
235264
public readonly struct GetLatestTransfersOutput : ISmartContractOutput<GetLatestTransfersOutput>
236265
{
237-
public static GetLatestTransfersOutput FromBytes(ReadOnlySpan<byte> data) => new();
266+
public GetLatestTransfersLatestTransfersEntry[] Entries { get; init; }
267+
268+
public static GetLatestTransfersOutput FromBytes(ReadOnlySpan<byte> data)
269+
{
270+
var entries = new GetLatestTransfersLatestTransfersEntry[128];
271+
for (int i = 0; i < 128; i++)
272+
{
273+
entries[i] = GetLatestTransfersLatestTransfersEntry.ReadFrom(data.Slice(0 + i * GetLatestTransfersLatestTransfersEntry.Size, GetLatestTransfersLatestTransfersEntry.Size));
274+
}
275+
return new GetLatestTransfersOutput
276+
{
277+
Entries = entries
278+
};
279+
}
238280
}
239281

240282
// ═══ Function: GetProposalFee (inputType=6) ═══
@@ -262,25 +304,83 @@ public static GetProposalFeeOutput FromBytes(ReadOnlySpan<byte> data)
262304

263305
// ═══ Function: GetRegularPayments (inputType=7) ═══
264306

307+
/// <summary>Nested type from GetRegularPayments.</summary>
308+
public readonly struct GetRegularPaymentsRegularPaymentEntry
309+
{
310+
public const int Size = 312;
311+
312+
public required byte[] Destination { get; init; }
313+
public byte[] Url { get; init; }
314+
public long Amount { get; init; }
315+
public uint Tick { get; init; }
316+
public int PeriodIndex { get; init; }
317+
public bool Success { get; init; }
318+
public byte[] _padding0 { get; init; }
319+
public byte[] _padding1 { get; init; }
320+
321+
public static GetRegularPaymentsRegularPaymentEntry ReadFrom(ReadOnlySpan<byte> data)
322+
{
323+
var url = new byte[256];
324+
for (int i = 0; i < 256; i++)
325+
{
326+
url[i] = data.Slice(32 + i * 1, 1)[0];
327+
}
328+
var _padding0 = new byte[1];
329+
for (int i = 0; i < 1; i++)
330+
{
331+
_padding0[i] = data.Slice(305 + i * 1, 1)[0];
332+
}
333+
var _padding1 = new byte[2];
334+
for (int i = 0; i < 2; i++)
335+
{
336+
_padding1[i] = data.Slice(306 + i * 1, 1)[0];
337+
}
338+
return new GetRegularPaymentsRegularPaymentEntry
339+
{
340+
Destination = data[0..].Slice(0, 32).ToArray(),
341+
Url = url,
342+
Amount = BinaryPrimitives.ReadInt64LittleEndian(data[288..]),
343+
Tick = BinaryPrimitives.ReadUInt32LittleEndian(data[296..]),
344+
PeriodIndex = BinaryPrimitives.ReadInt32LittleEndian(data[300..]),
345+
Success = (data.Slice(304, 1)[0] != 0),
346+
_padding0 = _padding0,
347+
_padding1 = _padding1
348+
};
349+
}
350+
}
351+
265352
/// <summary>Input for query (empty).</summary>
266353
public readonly struct GetRegularPaymentsInput : ISmartContractInput
267354
{
268355
public int SerializedSize => 0;
269356
public byte[] ToBytes() => [];
270357
}
271358

272-
/// <summary>Output (empty).</summary>
359+
/// <summary>Output.</summary>
273360
public readonly struct GetRegularPaymentsOutput : ISmartContractOutput<GetRegularPaymentsOutput>
274361
{
275-
public static GetRegularPaymentsOutput FromBytes(ReadOnlySpan<byte> data) => new();
362+
public GetRegularPaymentsRegularPaymentEntry[] Entries { get; init; }
363+
364+
public static GetRegularPaymentsOutput FromBytes(ReadOnlySpan<byte> data)
365+
{
366+
var entries = new GetRegularPaymentsRegularPaymentEntry[128];
367+
for (int i = 0; i < 128; i++)
368+
{
369+
entries[i] = GetRegularPaymentsRegularPaymentEntry.ReadFrom(data.Slice(0 + i * GetRegularPaymentsRegularPaymentEntry.Size, GetRegularPaymentsRegularPaymentEntry.Size));
370+
}
371+
return new GetRegularPaymentsOutput
372+
{
373+
Entries = entries
374+
};
375+
}
276376
}
277377

278378
// ═══ Procedure: SetProposal (inputType=1) ═══
279379

280380
/// <summary>Input payload for procedure.</summary>
281381
public sealed class SetProposalPayload : ITransactionPayload, ISmartContractInput
282382
{
283-
public const int Size = 20;
383+
public const int Size = 24;
284384

285385
public ushort InputType => 1;
286386
public ushort InputSize => Size;

src/Qubic.Core/Contracts/Generated/Gqmprop.g.cs

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public static class Procedures
4848
/// <summary>Input for query.</summary>
4949
public readonly struct GetProposalIndicesInput : ISmartContractInput
5050
{
51-
public const int Size = 5;
51+
public const int Size = 8;
5252

5353
public int SerializedSize => Size;
5454

@@ -59,7 +59,7 @@ public byte[] ToBytes()
5959
{
6060
var bytes = new byte[Size];
6161
bytes.AsSpan(0, 1)[0] = (byte)(ActiveProposals ? 1 : 0);
62-
BinaryPrimitives.WriteInt32LittleEndian(bytes.AsSpan(1), PrevProposalIndex);
62+
BinaryPrimitives.WriteInt32LittleEndian(bytes.AsSpan(4), PrevProposalIndex);
6363
return bytes;
6464
}
6565
}
@@ -148,7 +148,7 @@ public static GetProposalOutput FromBytes(ReadOnlySpan<byte> data)
148148
/// <summary>Input for query.</summary>
149149
public readonly struct GetVoteInput : ISmartContractInput
150150
{
151-
public const int Size = 34;
151+
public const int Size = 40;
152152

153153
public int SerializedSize => Size;
154154

@@ -217,17 +217,50 @@ public static GetVotingResultsOutput FromBytes(ReadOnlySpan<byte> data)
217217

218218
// ═══ Function: GetRevenueDonation (inputType=5) ═══
219219

220+
/// <summary>Nested type from GetRevenueDonation.</summary>
221+
public readonly struct GetRevenueDonationRevenueDonationEntry
222+
{
223+
public const int Size = 48;
224+
225+
public required byte[] DestinationPublicKey { get; init; }
226+
public long MillionthAmount { get; init; }
227+
public ushort FirstEpoch { get; init; }
228+
229+
public static GetRevenueDonationRevenueDonationEntry ReadFrom(ReadOnlySpan<byte> data)
230+
{
231+
return new GetRevenueDonationRevenueDonationEntry
232+
{
233+
DestinationPublicKey = data[0..].Slice(0, 32).ToArray(),
234+
MillionthAmount = BinaryPrimitives.ReadInt64LittleEndian(data[32..]),
235+
FirstEpoch = BinaryPrimitives.ReadUInt16LittleEndian(data[40..])
236+
};
237+
}
238+
}
239+
220240
/// <summary>Input for query (empty).</summary>
221241
public readonly struct GetRevenueDonationInput : ISmartContractInput
222242
{
223243
public int SerializedSize => 0;
224244
public byte[] ToBytes() => [];
225245
}
226246

227-
/// <summary>Output (empty).</summary>
247+
/// <summary>Output.</summary>
228248
public readonly struct GetRevenueDonationOutput : ISmartContractOutput<GetRevenueDonationOutput>
229249
{
230-
public static GetRevenueDonationOutput FromBytes(ReadOnlySpan<byte> data) => new();
250+
public GetRevenueDonationRevenueDonationEntry[] Entries { get; init; }
251+
252+
public static GetRevenueDonationOutput FromBytes(ReadOnlySpan<byte> data)
253+
{
254+
var entries = new GetRevenueDonationRevenueDonationEntry[128];
255+
for (int i = 0; i < 128; i++)
256+
{
257+
entries[i] = GetRevenueDonationRevenueDonationEntry.ReadFrom(data.Slice(0 + i * GetRevenueDonationRevenueDonationEntry.Size, GetRevenueDonationRevenueDonationEntry.Size));
258+
}
259+
return new GetRevenueDonationOutput
260+
{
261+
Entries = entries
262+
};
263+
}
231264
}
232265

233266
// ═══ Procedure: SetProposal (inputType=1) ═══

0 commit comments

Comments
 (0)