@xian-tech/client 0.1.0

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 ADDED
@@ -0,0 +1,16 @@
1
+ # @xian-tech/client
2
+
3
+ This package owns the typed Xian client surface for JS / TS consumers.
4
+
5
+ It includes:
6
+
7
+ - HTTP and ABCI query helpers
8
+ - transaction payload building, signing, and broadcast helpers
9
+ - Ed25519 signing primitives for tests and local development
10
+ - websocket subscriptions for dashboard state and event streams
11
+
12
+ It does not own:
13
+
14
+ - browser wallet discovery
15
+ - wallet-provider event contracts
16
+ - framework-specific bindings
@@ -0,0 +1,75 @@
1
+ import { WatchApi } from "./watch";
2
+ import type { BroadcastMode, BroadcastTxOptions, BuildTxRequest, ContractSendOptions, EstimateStampsOptions, EstimateStampsResult, SimulateRequest, TokenApproveOptions, TokenTransferOptions, TransactionReceipt, TransactionSubmission, WaitForTxOptions, XianClientOptions, XianSignedTransaction, XianSigner, XianUnsignedTransaction } from "./types";
3
+ export declare class XianClient {
4
+ readonly rpcUrl: string;
5
+ readonly dashboardUrl?: string;
6
+ readonly watch: WatchApi;
7
+ private readonly fetchFn;
8
+ private chainIdCache?;
9
+ constructor(options: XianClientOptions);
10
+ private requestJson;
11
+ private abciQuery;
12
+ private decodeAbciValue;
13
+ private normalizeTxLookup;
14
+ private decodeTxRecord;
15
+ getGenesis(): Promise<Record<string, unknown>>;
16
+ getChainId(): Promise<string>;
17
+ getStatus(): Promise<Record<string, unknown>>;
18
+ getBlock(height: number | string): Promise<Record<string, unknown>>;
19
+ getTx(txHash: string): Promise<TransactionReceipt>;
20
+ getNonce(address: string): Promise<number | bigint>;
21
+ getState(contract: string, variable: string, keys?: string[]): Promise<unknown>;
22
+ getBalance(address: string, options?: {
23
+ contract?: string;
24
+ }): Promise<unknown>;
25
+ getTokenMetadata(contract: string): Promise<{
26
+ contract: string;
27
+ name: string | null;
28
+ symbol: string | null;
29
+ logoUrl: string | null;
30
+ }>;
31
+ getStampRate(): Promise<number | bigint | null>;
32
+ getContract(contract: string): Promise<string | null>;
33
+ getContractCode(contract: string): Promise<string | null>;
34
+ simulate(request: SimulateRequest): Promise<Record<string, unknown>>;
35
+ call(request: SimulateRequest): Promise<unknown>;
36
+ estimateStamps(request: SimulateRequest, options?: EstimateStampsOptions): Promise<EstimateStampsResult>;
37
+ buildTx(request: BuildTxRequest): Promise<XianUnsignedTransaction>;
38
+ signTx(tx: XianUnsignedTransaction, signer: XianSigner): Promise<XianSignedTransaction>;
39
+ broadcastTx(tx: XianSignedTransaction, options?: BroadcastTxOptions): Promise<TransactionSubmission>;
40
+ waitForTx(txHash: string, options?: WaitForTxOptions): Promise<TransactionReceipt>;
41
+ sendTx(request: BuildTxRequest & {
42
+ signer: XianSigner;
43
+ mode?: BroadcastMode;
44
+ waitForTx?: boolean;
45
+ timeoutMs?: number;
46
+ pollIntervalMs?: number;
47
+ }): Promise<TransactionSubmission>;
48
+ contract(name: string): ContractClient;
49
+ token(name?: string): TokenClient;
50
+ }
51
+ export declare class ContractClient {
52
+ private readonly client;
53
+ private readonly contractName;
54
+ constructor(client: XianClient, contractName: string);
55
+ call(functionName: string, kwargs: Record<string, unknown>, sender?: string): Promise<unknown>;
56
+ send(functionName: string, kwargs: Record<string, unknown>, options: ContractSendOptions & {
57
+ signer: XianSigner;
58
+ }): Promise<TransactionSubmission>;
59
+ }
60
+ export declare class TokenClient {
61
+ private readonly client;
62
+ private readonly tokenName;
63
+ constructor(client: XianClient, tokenName: string);
64
+ balanceOf(address: string): Promise<unknown>;
65
+ metadata(): Promise<{
66
+ contract: string;
67
+ name: string | null;
68
+ symbol: string | null;
69
+ logoUrl: string | null;
70
+ }>;
71
+ allowance(owner: string, spender: string): Promise<unknown>;
72
+ transfer(options: TokenTransferOptions): Promise<TransactionSubmission>;
73
+ approve(options: TokenApproveOptions): Promise<TransactionSubmission>;
74
+ }
75
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/client.js ADDED
@@ -0,0 +1,580 @@
1
+ import { base64ToUtf8, bytesToHex, canonicalizeRuntime, decodeRuntime, encodeRuntime, parseXianNumber, sortKeysDeep, utf8ToBytes } from "./encoding";
2
+ import { isValidEd25519Signature } from "./ed25519";
3
+ import { AbciError, RpcError, SimulationError, TransactionError, TransportError, TxTimeoutError } from "./errors";
4
+ import { WatchApi } from "./watch";
5
+ const EMPTY_ABCI_VALUE = "AA==";
6
+ const DEFAULT_TIMEOUT_MS = 30_000;
7
+ const DEFAULT_POLL_INTERVAL_MS = 500;
8
+ const DEFAULT_STAMP_MARGIN = 0.2;
9
+ const DEFAULT_MIN_STAMP_HEADROOM = 5_000;
10
+ function stripTrailingSlash(value) {
11
+ return value.replace(/\/+$/, "");
12
+ }
13
+ function sleep(ms) {
14
+ return new Promise((resolve) => setTimeout(resolve, ms));
15
+ }
16
+ function isIdentifier(value) {
17
+ return /^[a-zA-Z][a-zA-Z0-9_]*$/.test(value);
18
+ }
19
+ function isNonNegativeInteger(value) {
20
+ if (typeof value === "bigint") {
21
+ return value >= 0n;
22
+ }
23
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
24
+ }
25
+ function isHexKey(value) {
26
+ return /^[a-fA-F0-9]{64}$/.test(value);
27
+ }
28
+ function readErrorMessage(value) {
29
+ const data = value.data;
30
+ if (typeof data === "string" && data.length > 0) {
31
+ return data;
32
+ }
33
+ const message = value.message;
34
+ if (typeof message === "string" && message.length > 0) {
35
+ return message;
36
+ }
37
+ return "RPC error";
38
+ }
39
+ function asRecord(value) {
40
+ if (typeof value !== "object" || value === null) {
41
+ throw new TransportError("expected object response");
42
+ }
43
+ return value;
44
+ }
45
+ function parseJsonResult(value) {
46
+ try {
47
+ return JSON.parse(value);
48
+ }
49
+ catch {
50
+ return value;
51
+ }
52
+ }
53
+ function normalizeMaybeNumber(value) {
54
+ if (typeof value !== "string") {
55
+ return value;
56
+ }
57
+ if (/^-?\d+$/.test(value)) {
58
+ return parseXianNumber(value);
59
+ }
60
+ return value;
61
+ }
62
+ function normalizeMaybeString(value) {
63
+ if (value == null) {
64
+ return null;
65
+ }
66
+ return typeof value === "string" ? value : String(value);
67
+ }
68
+ function isPendingLookupReceipt(receipt) {
69
+ return !receipt.success && receipt.txHash == null && receipt.transaction == null;
70
+ }
71
+ function validatePayload(payload) {
72
+ const keys = Object.keys(payload).sort();
73
+ const expected = ["chain_id", "contract", "function", "kwargs", "nonce", "sender", "stamps_supplied"];
74
+ if (keys.join(",") !== expected.join(",")) {
75
+ throw new TransactionError("payload must contain the canonical Xian transaction keys");
76
+ }
77
+ if (!isHexKey(payload.sender)) {
78
+ throw new TransactionError("sender must be a 32-byte hex public key");
79
+ }
80
+ if (!isIdentifier(payload.contract)) {
81
+ throw new TransactionError("contract must be a valid identifier");
82
+ }
83
+ if (!isIdentifier(payload.function)) {
84
+ throw new TransactionError("function must be a valid identifier");
85
+ }
86
+ if (!isNonNegativeInteger(payload.nonce)) {
87
+ throw new TransactionError("nonce must be a non-negative integer");
88
+ }
89
+ if (!isNonNegativeInteger(payload.stamps_supplied)) {
90
+ throw new TransactionError("stamps_supplied must be a non-negative integer");
91
+ }
92
+ for (const key of Object.keys(payload.kwargs)) {
93
+ if (!isIdentifier(key)) {
94
+ throw new TransactionError("kwargs keys must be valid identifiers");
95
+ }
96
+ }
97
+ }
98
+ export class XianClient {
99
+ rpcUrl;
100
+ dashboardUrl;
101
+ watch;
102
+ fetchFn;
103
+ chainIdCache;
104
+ constructor(options) {
105
+ this.rpcUrl = stripTrailingSlash(options.rpcUrl);
106
+ this.dashboardUrl = options.dashboardUrl
107
+ ? stripTrailingSlash(options.dashboardUrl)
108
+ : undefined;
109
+ this.chainIdCache = options.chainId;
110
+ this.fetchFn = options.fetchFn ?? globalThis.fetch.bind(globalThis);
111
+ this.watch = new WatchApi({
112
+ dashboardUrl: this.dashboardUrl,
113
+ webSocketFactory: options.webSocketFactory
114
+ });
115
+ }
116
+ async requestJson(method, url) {
117
+ let response;
118
+ try {
119
+ response = await this.fetchFn(url, { method });
120
+ }
121
+ catch (error) {
122
+ throw new TransportError(`request failed for ${url}`, { cause: error });
123
+ }
124
+ if (!response.ok) {
125
+ throw new TransportError(`${method} ${url} returned ${response.status}`);
126
+ }
127
+ try {
128
+ return asRecord(await response.json());
129
+ }
130
+ catch (error) {
131
+ throw new TransportError(`invalid JSON response from ${url}`, { cause: error });
132
+ }
133
+ }
134
+ async abciQuery(path) {
135
+ const url = new URL(`${this.rpcUrl}/abci_query`);
136
+ url.searchParams.set("path", `"${path}"`);
137
+ const data = await this.requestJson("POST", url.toString());
138
+ if ("error" in data) {
139
+ throw new RpcError(readErrorMessage(asRecord(data.error)), data.error);
140
+ }
141
+ const response = asRecord(asRecord(data.result).response);
142
+ const code = response.code;
143
+ if (typeof code === "number" && code !== 0) {
144
+ throw new AbciError(String(response.log ?? "ABCI query failed"), {
145
+ path,
146
+ response
147
+ });
148
+ }
149
+ return data;
150
+ }
151
+ decodeAbciValue(value) {
152
+ if (value == null || value === EMPTY_ABCI_VALUE) {
153
+ return null;
154
+ }
155
+ const decodedUtf8 = base64ToUtf8(String(value));
156
+ const decoded = decodeRuntime(decodedUtf8);
157
+ if (decoded == null) {
158
+ return normalizeMaybeNumber(decodedUtf8);
159
+ }
160
+ return normalizeMaybeNumber(decoded);
161
+ }
162
+ normalizeTxLookup(data) {
163
+ const result = asRecord(data.result ?? {});
164
+ const txResult = asRecord(result.tx_result ?? {});
165
+ const execution = txResult.data == null ? undefined : parseJsonResult(base64ToUtf8(String(txResult.data)));
166
+ const transaction = result.tx == null ? undefined : this.decodeTxRecord(String(result.tx));
167
+ if ("error" in data) {
168
+ return {
169
+ success: false,
170
+ message: readErrorMessage(asRecord(data.error)),
171
+ response: data
172
+ };
173
+ }
174
+ const code = txResult.code;
175
+ const success = typeof code === "number" ? code === 0 : false;
176
+ const txHash = typeof result.hash === "string" ? result.hash : undefined;
177
+ return {
178
+ success,
179
+ txHash,
180
+ message: success ? undefined : execution ?? txResult.log ?? "Transaction failed",
181
+ response: data,
182
+ transaction,
183
+ execution
184
+ };
185
+ }
186
+ decodeTxRecord(value) {
187
+ const hexPayload = base64ToUtf8(value);
188
+ const decoded = decodeRuntime(hexPayload);
189
+ if (decoded != null) {
190
+ return decoded;
191
+ }
192
+ return decodeRuntime(hexToUtf8(hexPayload));
193
+ }
194
+ async getGenesis() {
195
+ return this.requestJson("GET", `${this.rpcUrl}/genesis`);
196
+ }
197
+ async getChainId() {
198
+ if (this.chainIdCache) {
199
+ return this.chainIdCache;
200
+ }
201
+ const genesis = await this.getGenesis();
202
+ const chainId = asRecord(asRecord(genesis.result).genesis).chain_id;
203
+ if (typeof chainId !== "string" || chainId.length === 0) {
204
+ throw new RpcError("genesis response did not include chain_id", genesis);
205
+ }
206
+ this.chainIdCache = chainId;
207
+ return chainId;
208
+ }
209
+ async getStatus() {
210
+ return this.requestJson("GET", `${this.rpcUrl}/status`);
211
+ }
212
+ async getBlock(height) {
213
+ return this.requestJson("GET", `${this.rpcUrl}/block?height=${height}`);
214
+ }
215
+ async getTx(txHash) {
216
+ const data = await this.requestJson("GET", `${this.rpcUrl}/tx?hash=0x${txHash}`);
217
+ return this.normalizeTxLookup(data);
218
+ }
219
+ async getNonce(address) {
220
+ const data = await this.abciQuery(`/get_next_nonce/${address}`);
221
+ const value = asRecord(asRecord(data.result).response).value;
222
+ if (value == null || value === EMPTY_ABCI_VALUE) {
223
+ return 0;
224
+ }
225
+ return parseXianNumber(base64ToUtf8(String(value)));
226
+ }
227
+ async getState(contract, variable, keys = []) {
228
+ const suffix = keys.length > 0 ? `:${keys.join(":")}` : "";
229
+ const data = await this.abciQuery(`/get/${contract}.${variable}${suffix}`);
230
+ return this.decodeAbciValue(asRecord(asRecord(data.result).response).value);
231
+ }
232
+ async getBalance(address, options) {
233
+ const contract = options?.contract ?? "currency";
234
+ try {
235
+ const simulation = await this.simulate({
236
+ sender: address,
237
+ contract,
238
+ function: "balance_of",
239
+ kwargs: { address }
240
+ });
241
+ return normalizeMaybeNumber(simulation.result);
242
+ }
243
+ catch {
244
+ return this.getState(contract, "balances", [address]);
245
+ }
246
+ }
247
+ async getTokenMetadata(contract) {
248
+ if (contract === "currency") {
249
+ return {
250
+ contract,
251
+ name: "Xian",
252
+ symbol: "Xian",
253
+ logoUrl: null
254
+ };
255
+ }
256
+ const [name, symbol, logoUrl] = await Promise.all([
257
+ this.getState(contract, "metadata", ["token_name"]),
258
+ this.getState(contract, "metadata", ["token_symbol"]),
259
+ this.getState(contract, "metadata", ["token_logo_url"])
260
+ ]);
261
+ return {
262
+ contract,
263
+ name: normalizeMaybeString(name),
264
+ symbol: normalizeMaybeString(symbol),
265
+ logoUrl: normalizeMaybeString(logoUrl)
266
+ };
267
+ }
268
+ async getStampRate() {
269
+ const value = await this.getState("stamp_cost", "S", ["value"]);
270
+ if (value == null) {
271
+ return null;
272
+ }
273
+ if (typeof value === "number" || typeof value === "bigint") {
274
+ return value;
275
+ }
276
+ return parseXianNumber(String(value));
277
+ }
278
+ async getContract(contract) {
279
+ const data = await this.abciQuery(`/contract/${contract}`);
280
+ const value = asRecord(asRecord(data.result).response).value;
281
+ if (value == null || value === EMPTY_ABCI_VALUE) {
282
+ return null;
283
+ }
284
+ return base64ToUtf8(String(value));
285
+ }
286
+ async getContractCode(contract) {
287
+ const data = await this.abciQuery(`/contract_code/${contract}`);
288
+ const value = asRecord(asRecord(data.result).response).value;
289
+ if (value == null || value === EMPTY_ABCI_VALUE) {
290
+ return null;
291
+ }
292
+ return base64ToUtf8(String(value));
293
+ }
294
+ async simulate(request) {
295
+ const payload = sortKeysDeep({
296
+ contract: request.contract,
297
+ function: request.function,
298
+ kwargs: request.kwargs,
299
+ sender: request.sender
300
+ });
301
+ const encoded = bytesToHex(utf8ToBytes(encodeRuntime(payload)));
302
+ const data = await this.abciQuery(`/simulate_tx/${encoded}`);
303
+ const response = asRecord(asRecord(data.result).response);
304
+ if (response.code != null && response.code !== 0) {
305
+ throw new SimulationError(String(response.log ?? "simulation failed"), response);
306
+ }
307
+ const rawValue = response.value;
308
+ const decoded = rawValue == null ? null : decodeRuntime(base64ToUtf8(String(rawValue)));
309
+ if (decoded == null) {
310
+ throw new SimulationError("simulation response did not decode to JSON", response);
311
+ }
312
+ return decoded;
313
+ }
314
+ async call(request) {
315
+ const simulation = await this.simulate(request);
316
+ const status = simulation.status;
317
+ if (status != null && status !== 0) {
318
+ throw new SimulationError(String(simulation.result ?? "Simulation failed"), simulation);
319
+ }
320
+ return simulation.result;
321
+ }
322
+ async estimateStamps(request, options) {
323
+ const simulation = await this.simulate(request);
324
+ const rawStamps = simulation.stamps_used;
325
+ const estimated = typeof rawStamps === "number"
326
+ ? rawStamps
327
+ : Number.parseInt(String(rawStamps ?? "0"), 10);
328
+ const stampMargin = options?.stampMargin ?? DEFAULT_STAMP_MARGIN;
329
+ const minStampHeadroom = options?.minStampHeadroom ?? DEFAULT_MIN_STAMP_HEADROOM;
330
+ const proportional = Math.ceil(estimated * stampMargin);
331
+ const suggested = estimated + Math.max(proportional, minStampHeadroom);
332
+ return {
333
+ estimated,
334
+ suggested,
335
+ simulation
336
+ };
337
+ }
338
+ async buildTx(request) {
339
+ const chainId = request.chainId ?? (await this.getChainId());
340
+ const nonce = request.nonce ?? (await this.getNonce(request.sender));
341
+ let stampsSupplied = request.stampsSupplied ?? request.stamps;
342
+ if (stampsSupplied == null) {
343
+ const estimate = await this.estimateStamps({
344
+ sender: request.sender,
345
+ contract: request.contract,
346
+ function: request.function,
347
+ kwargs: request.kwargs
348
+ });
349
+ stampsSupplied = estimate.suggested;
350
+ }
351
+ const payload = sortKeysDeep({
352
+ chain_id: chainId,
353
+ contract: request.contract,
354
+ function: request.function,
355
+ kwargs: request.kwargs,
356
+ nonce,
357
+ sender: request.sender,
358
+ stamps_supplied: stampsSupplied
359
+ });
360
+ validatePayload(payload);
361
+ return { payload };
362
+ }
363
+ async signTx(tx, signer) {
364
+ validatePayload(tx.payload);
365
+ const signerAddress = typeof signer.getAddress === "function" ? await signer.getAddress() : undefined;
366
+ if (signerAddress && signerAddress !== tx.payload.sender) {
367
+ throw new TransactionError("signer address does not match payload sender");
368
+ }
369
+ const signature = await signer.signMessage(canonicalizeRuntime(tx.payload));
370
+ if (!isValidEd25519Signature(signature)) {
371
+ throw new TransactionError("signer returned an invalid Ed25519 signature");
372
+ }
373
+ return sortKeysDeep({
374
+ metadata: { signature },
375
+ payload: tx.payload
376
+ });
377
+ }
378
+ async broadcastTx(tx, options) {
379
+ validatePayload(tx.payload);
380
+ const mode = options?.mode ?? "checktx";
381
+ if (!["async", "checktx", "commit"].includes(mode)) {
382
+ throw new TransactionError("mode must be one of: async, checktx, commit");
383
+ }
384
+ const endpoint = mode === "async"
385
+ ? "broadcast_tx_async"
386
+ : mode === "commit"
387
+ ? "broadcast_tx_commit"
388
+ : "broadcast_tx_sync";
389
+ const encodedTx = bytesToHex(utf8ToBytes(encodeRuntime(sortKeysDeep(tx))));
390
+ const url = new URL(`${this.rpcUrl}/${endpoint}`);
391
+ url.searchParams.set("tx", `"${encodedTx}"`);
392
+ const data = await this.requestJson("POST", url.toString());
393
+ const response = asRecord(data.result ?? {});
394
+ const txHash = typeof response.hash === "string" ? response.hash : undefined;
395
+ const submission = {
396
+ submitted: !("error" in data),
397
+ accepted: null,
398
+ finalized: false,
399
+ txHash,
400
+ message: "error" in data ? readErrorMessage(asRecord(data.error)) : undefined,
401
+ mode,
402
+ nonce: tx.payload.nonce,
403
+ stampsSupplied: tx.payload.stamps_supplied,
404
+ response: data
405
+ };
406
+ if ("error" in data) {
407
+ return submission;
408
+ }
409
+ if (mode === "commit") {
410
+ const checkTx = asRecord(response.check_tx ?? {});
411
+ const deliverTx = asRecord(response.deliver_tx ?? response.tx_result ?? {});
412
+ submission.accepted = checkTx.code === 0;
413
+ submission.finalized = submission.accepted && deliverTx.code === 0 && String(response.height ?? "0") !== "0";
414
+ submission.message = submission.accepted
415
+ ? submission.finalized
416
+ ? undefined
417
+ : deliverTx.log ?? "transaction was not finalized"
418
+ : checkTx.log ?? "CheckTx failed";
419
+ return submission;
420
+ }
421
+ if (mode === "async") {
422
+ if (options?.waitForTx && submission.txHash) {
423
+ submission.receipt = await this.waitForTx(submission.txHash, options);
424
+ submission.finalized = true;
425
+ }
426
+ return submission;
427
+ }
428
+ submission.accepted = response.code === 0;
429
+ submission.message = submission.accepted ? undefined : response.log ?? "CheckTx failed";
430
+ if (submission.accepted && options?.waitForTx && submission.txHash) {
431
+ submission.receipt = await this.waitForTx(submission.txHash, options);
432
+ submission.finalized = true;
433
+ }
434
+ return submission;
435
+ }
436
+ async waitForTx(txHash, options) {
437
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
438
+ const pollIntervalMs = options?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
439
+ const deadline = Date.now() + timeoutMs;
440
+ let lastError;
441
+ while (Date.now() < deadline) {
442
+ try {
443
+ const receipt = await this.getTx(txHash);
444
+ if (!isPendingLookupReceipt(receipt)) {
445
+ return receipt;
446
+ }
447
+ }
448
+ catch (error) {
449
+ lastError = error;
450
+ }
451
+ await sleep(pollIntervalMs);
452
+ }
453
+ throw new TxTimeoutError(`timed out waiting for transaction ${txHash}`, {
454
+ cause: lastError
455
+ });
456
+ }
457
+ async sendTx(request) {
458
+ const tx = await this.buildTx(request);
459
+ const signedTx = await this.signTx(tx, request.signer);
460
+ return this.broadcastTx(signedTx, {
461
+ mode: request.mode,
462
+ waitForTx: request.waitForTx,
463
+ timeoutMs: request.timeoutMs,
464
+ pollIntervalMs: request.pollIntervalMs
465
+ });
466
+ }
467
+ contract(name) {
468
+ return new ContractClient(this, name);
469
+ }
470
+ token(name = "currency") {
471
+ return new TokenClient(this, name);
472
+ }
473
+ }
474
+ function hexToUtf8(value) {
475
+ const out = new Uint8Array(value.length / 2);
476
+ for (let index = 0; index < value.length; index += 2) {
477
+ out[index / 2] = Number.parseInt(value.slice(index, index + 2), 16);
478
+ }
479
+ return new TextDecoder().decode(out);
480
+ }
481
+ export class ContractClient {
482
+ client;
483
+ contractName;
484
+ constructor(client, contractName) {
485
+ this.client = client;
486
+ this.contractName = contractName;
487
+ }
488
+ async call(functionName, kwargs, sender = "0".repeat(64)) {
489
+ return this.client.call({
490
+ sender,
491
+ contract: this.contractName,
492
+ function: functionName,
493
+ kwargs
494
+ });
495
+ }
496
+ async send(functionName, kwargs, options) {
497
+ if (typeof options.signer.getAddress !== "function") {
498
+ throw new TransactionError("signer.getAddress() is required for contract sends");
499
+ }
500
+ const sender = await options.signer.getAddress();
501
+ return this.client.sendTx({
502
+ sender,
503
+ contract: this.contractName,
504
+ function: functionName,
505
+ kwargs,
506
+ signer: options.signer,
507
+ mode: options.mode,
508
+ waitForTx: options.waitForTx,
509
+ timeoutMs: options.timeoutMs,
510
+ pollIntervalMs: options.pollIntervalMs,
511
+ stamps: options.stamps,
512
+ nonce: options.nonce,
513
+ chainId: options.chainId
514
+ });
515
+ }
516
+ }
517
+ export class TokenClient {
518
+ client;
519
+ tokenName;
520
+ constructor(client, tokenName) {
521
+ this.client = client;
522
+ this.tokenName = tokenName;
523
+ }
524
+ balanceOf(address) {
525
+ return this.client.getBalance(address, { contract: this.tokenName });
526
+ }
527
+ metadata() {
528
+ return this.client.getTokenMetadata(this.tokenName);
529
+ }
530
+ allowance(owner, spender) {
531
+ return this.client.getState(this.tokenName, "approvals", [owner, spender]);
532
+ }
533
+ async transfer(options) {
534
+ if (typeof options.signer.getAddress !== "function") {
535
+ throw new TransactionError("signer.getAddress() is required for token transfer");
536
+ }
537
+ const sender = await options.signer.getAddress();
538
+ return this.client.sendTx({
539
+ sender,
540
+ contract: this.tokenName,
541
+ function: "transfer",
542
+ kwargs: {
543
+ to: options.to,
544
+ amount: options.amount
545
+ },
546
+ signer: options.signer,
547
+ mode: options.mode,
548
+ waitForTx: options.waitForTx,
549
+ timeoutMs: options.timeoutMs,
550
+ pollIntervalMs: options.pollIntervalMs,
551
+ stamps: options.stamps,
552
+ nonce: options.nonce,
553
+ chainId: options.chainId
554
+ });
555
+ }
556
+ async approve(options) {
557
+ if (typeof options.signer.getAddress !== "function") {
558
+ throw new TransactionError("signer.getAddress() is required for token approve");
559
+ }
560
+ const sender = await options.signer.getAddress();
561
+ return this.client.sendTx({
562
+ sender,
563
+ contract: this.tokenName,
564
+ function: "approve",
565
+ kwargs: {
566
+ to: options.spender,
567
+ amount: options.amount
568
+ },
569
+ signer: options.signer,
570
+ mode: options.mode,
571
+ waitForTx: options.waitForTx,
572
+ timeoutMs: options.timeoutMs,
573
+ pollIntervalMs: options.pollIntervalMs,
574
+ stamps: options.stamps,
575
+ nonce: options.nonce,
576
+ chainId: options.chainId
577
+ });
578
+ }
579
+ }
580
+ //# sourceMappingURL=client.js.map