= T extends T ? keyof T : never;
263 | export type Exact = P extends Builtin
264 | ? P
265 | : P & { [K in keyof P]: Exact
} & {
266 | [K in Exclude>]: never;
267 | };
268 |
269 | function isSet(value: any): boolean {
270 | return value !== null && value !== undefined;
271 | }
272 |
273 | export interface MessageFns {
274 | encode(message: T, writer?: BinaryWriter): BinaryWriter;
275 | decode(input: BinaryReader | Uint8Array, length?: number): T;
276 | fromJSON(object: any): T;
277 | toJSON(message: T): unknown;
278 | create, I>>(base?: I): T;
279 | fromPartial, I>>(object: I): T;
280 | }
281 |
--------------------------------------------------------------------------------
/src/proto/index.ts:
--------------------------------------------------------------------------------
1 | export * from './tm2/tx';
2 | export { Any } from './google/protobuf/any';
3 |
--------------------------------------------------------------------------------
/src/proto/tm2/abci.ts:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-ts_proto. DO NOT EDIT.
2 | // versions:
3 | // protoc-gen-ts_proto v2.2.0
4 | // protoc v5.29.0
5 | // source: tm2/abci.proto
6 |
7 | /* eslint-disable */
8 | import { BinaryReader, BinaryWriter } from '@bufbuild/protobuf/wire';
9 | import Long from 'long';
10 | import { Any } from '../google/protobuf/any';
11 |
12 | export const protobufPackage = 'tm2.abci';
13 |
14 | export interface ResponseDeliverTx {
15 | responseBase?: ResponseBase | undefined;
16 | gasWanted: Long;
17 | gasUsed: Long;
18 | }
19 |
20 | export interface ResponseBase {
21 | error?: Any | undefined;
22 | data: Uint8Array;
23 | events: Any[];
24 | log: string;
25 | info: string;
26 | }
27 |
28 | function createBaseResponseDeliverTx(): ResponseDeliverTx {
29 | return { responseBase: undefined, gasWanted: Long.ZERO, gasUsed: Long.ZERO };
30 | }
31 |
32 | export const ResponseDeliverTx: MessageFns = {
33 | encode(
34 | message: ResponseDeliverTx,
35 | writer: BinaryWriter = new BinaryWriter()
36 | ): BinaryWriter {
37 | if (message.responseBase !== undefined) {
38 | ResponseBase.encode(
39 | message.responseBase,
40 | writer.uint32(10).fork()
41 | ).join();
42 | }
43 | if (!message.gasWanted.equals(Long.ZERO)) {
44 | writer.uint32(16).sint64(message.gasWanted.toString());
45 | }
46 | if (!message.gasUsed.equals(Long.ZERO)) {
47 | writer.uint32(24).sint64(message.gasUsed.toString());
48 | }
49 | return writer;
50 | },
51 |
52 | decode(input: BinaryReader | Uint8Array, length?: number): ResponseDeliverTx {
53 | const reader =
54 | input instanceof BinaryReader ? input : new BinaryReader(input);
55 | let end = length === undefined ? reader.len : reader.pos + length;
56 | const message = createBaseResponseDeliverTx();
57 | while (reader.pos < end) {
58 | const tag = reader.uint32();
59 | switch (tag >>> 3) {
60 | case 1:
61 | if (tag !== 10) {
62 | break;
63 | }
64 |
65 | message.responseBase = ResponseBase.decode(reader, reader.uint32());
66 | continue;
67 | case 2:
68 | if (tag !== 16) {
69 | break;
70 | }
71 |
72 | message.gasWanted = Long.fromString(reader.sint64().toString());
73 | continue;
74 | case 3:
75 | if (tag !== 24) {
76 | break;
77 | }
78 |
79 | message.gasUsed = Long.fromString(reader.sint64().toString());
80 | continue;
81 | }
82 | if ((tag & 7) === 4 || tag === 0) {
83 | break;
84 | }
85 | reader.skip(tag & 7);
86 | }
87 | return message;
88 | },
89 |
90 | fromJSON(object: any): ResponseDeliverTx {
91 | return {
92 | responseBase: isSet(object.ResponseBase)
93 | ? ResponseBase.fromJSON(object.ResponseBase)
94 | : undefined,
95 | gasWanted: isSet(object.GasWanted)
96 | ? Long.fromValue(object.GasWanted)
97 | : Long.ZERO,
98 | gasUsed: isSet(object.GasUsed)
99 | ? Long.fromValue(object.GasUsed)
100 | : Long.ZERO,
101 | };
102 | },
103 |
104 | toJSON(message: ResponseDeliverTx): unknown {
105 | const obj: any = {};
106 | if (message.responseBase !== undefined) {
107 | obj.ResponseBase = ResponseBase.toJSON(message.responseBase);
108 | }
109 | if (message.gasWanted !== undefined) {
110 | obj.GasWanted = (message.gasWanted || Long.ZERO).toString();
111 | }
112 | if (message.gasUsed !== undefined) {
113 | obj.GasUsed = (message.gasUsed || Long.ZERO).toString();
114 | }
115 | return obj;
116 | },
117 |
118 | create, I>>(
119 | base?: I
120 | ): ResponseDeliverTx {
121 | return ResponseDeliverTx.fromPartial(base ?? ({} as any));
122 | },
123 | fromPartial, I>>(
124 | object: I
125 | ): ResponseDeliverTx {
126 | const message = createBaseResponseDeliverTx();
127 | message.responseBase =
128 | object.responseBase !== undefined && object.responseBase !== null
129 | ? ResponseBase.fromPartial(object.responseBase)
130 | : undefined;
131 | message.gasWanted =
132 | object.gasWanted !== undefined && object.gasWanted !== null
133 | ? Long.fromValue(object.gasWanted)
134 | : Long.ZERO;
135 | message.gasUsed =
136 | object.gasUsed !== undefined && object.gasUsed !== null
137 | ? Long.fromValue(object.gasUsed)
138 | : Long.ZERO;
139 | return message;
140 | },
141 | };
142 |
143 | function createBaseResponseBase(): ResponseBase {
144 | return {
145 | error: undefined,
146 | data: new Uint8Array(0),
147 | events: [],
148 | log: '',
149 | info: '',
150 | };
151 | }
152 |
153 | export const ResponseBase: MessageFns = {
154 | encode(
155 | message: ResponseBase,
156 | writer: BinaryWriter = new BinaryWriter()
157 | ): BinaryWriter {
158 | if (message.error !== undefined) {
159 | Any.encode(message.error, writer.uint32(10).fork()).join();
160 | }
161 | if (message.data.length !== 0) {
162 | writer.uint32(18).bytes(message.data);
163 | }
164 | for (const v of message.events) {
165 | Any.encode(v!, writer.uint32(26).fork()).join();
166 | }
167 | if (message.log !== '') {
168 | writer.uint32(34).string(message.log);
169 | }
170 | if (message.info !== '') {
171 | writer.uint32(42).string(message.info);
172 | }
173 | return writer;
174 | },
175 |
176 | decode(input: BinaryReader | Uint8Array, length?: number): ResponseBase {
177 | const reader =
178 | input instanceof BinaryReader ? input : new BinaryReader(input);
179 | let end = length === undefined ? reader.len : reader.pos + length;
180 | const message = createBaseResponseBase();
181 | while (reader.pos < end) {
182 | const tag = reader.uint32();
183 | switch (tag >>> 3) {
184 | case 1:
185 | if (tag !== 10) {
186 | break;
187 | }
188 |
189 | message.error = Any.decode(reader, reader.uint32());
190 | continue;
191 | case 2:
192 | if (tag !== 18) {
193 | break;
194 | }
195 |
196 | message.data = reader.bytes();
197 | continue;
198 | case 3:
199 | if (tag !== 26) {
200 | break;
201 | }
202 |
203 | message.events.push(Any.decode(reader, reader.uint32()));
204 | continue;
205 | case 4:
206 | if (tag !== 34) {
207 | break;
208 | }
209 |
210 | message.log = reader.string();
211 | continue;
212 | case 5:
213 | if (tag !== 42) {
214 | break;
215 | }
216 |
217 | message.info = reader.string();
218 | continue;
219 | }
220 | if ((tag & 7) === 4 || tag === 0) {
221 | break;
222 | }
223 | reader.skip(tag & 7);
224 | }
225 | return message;
226 | },
227 |
228 | fromJSON(object: any): ResponseBase {
229 | return {
230 | error: isSet(object.Error) ? Any.fromJSON(object.Error) : undefined,
231 | data: isSet(object.Data)
232 | ? bytesFromBase64(object.Data)
233 | : new Uint8Array(0),
234 | events: globalThis.Array.isArray(object?.Events)
235 | ? object.Events.map((e: any) => Any.fromJSON(e))
236 | : [],
237 | log: isSet(object.Log) ? globalThis.String(object.Log) : '',
238 | info: isSet(object.Info) ? globalThis.String(object.Info) : '',
239 | };
240 | },
241 |
242 | toJSON(message: ResponseBase): unknown {
243 | const obj: any = {};
244 | if (message.error !== undefined) {
245 | obj.Error = Any.toJSON(message.error);
246 | }
247 | if (message.data !== undefined) {
248 | obj.Data = base64FromBytes(message.data);
249 | }
250 | if (message.events?.length) {
251 | obj.Events = message.events.map((e) => Any.toJSON(e));
252 | }
253 | if (message.log !== undefined) {
254 | obj.Log = message.log;
255 | }
256 | if (message.info !== undefined) {
257 | obj.Info = message.info;
258 | }
259 | return obj;
260 | },
261 |
262 | create, I>>(
263 | base?: I
264 | ): ResponseBase {
265 | return ResponseBase.fromPartial(base ?? ({} as any));
266 | },
267 | fromPartial, I>>(
268 | object: I
269 | ): ResponseBase {
270 | const message = createBaseResponseBase();
271 | message.error =
272 | object.error !== undefined && object.error !== null
273 | ? Any.fromPartial(object.error)
274 | : undefined;
275 | message.data = object.data ?? new Uint8Array(0);
276 | message.events = object.events?.map((e) => Any.fromPartial(e)) || [];
277 | message.log = object.log ?? '';
278 | message.info = object.info ?? '';
279 | return message;
280 | },
281 | };
282 |
283 | function bytesFromBase64(b64: string): Uint8Array {
284 | if ((globalThis as any).Buffer) {
285 | return Uint8Array.from(globalThis.Buffer.from(b64, 'base64'));
286 | } else {
287 | const bin = globalThis.atob(b64);
288 | const arr = new Uint8Array(bin.length);
289 | for (let i = 0; i < bin.length; ++i) {
290 | arr[i] = bin.charCodeAt(i);
291 | }
292 | return arr;
293 | }
294 | }
295 |
296 | function base64FromBytes(arr: Uint8Array): string {
297 | if ((globalThis as any).Buffer) {
298 | return globalThis.Buffer.from(arr).toString('base64');
299 | } else {
300 | const bin: string[] = [];
301 | arr.forEach((byte) => {
302 | bin.push(globalThis.String.fromCharCode(byte));
303 | });
304 | return globalThis.btoa(bin.join(''));
305 | }
306 | }
307 |
308 | type Builtin =
309 | | Date
310 | | Function
311 | | Uint8Array
312 | | string
313 | | number
314 | | boolean
315 | | undefined;
316 |
317 | export type DeepPartial = T extends Builtin
318 | ? T
319 | : T extends Long
320 | ? string | number | Long
321 | : T extends globalThis.Array
322 | ? globalThis.Array>
323 | : T extends ReadonlyArray
324 | ? ReadonlyArray>
325 | : T extends {}
326 | ? { [K in keyof T]?: DeepPartial }
327 | : Partial;
328 |
329 | type KeysOfUnion = T extends T ? keyof T : never;
330 | export type Exact = P extends Builtin
331 | ? P
332 | : P & { [K in keyof P]: Exact
} & {
333 | [K in Exclude>]: never;
334 | };
335 |
336 | function isSet(value: any): boolean {
337 | return value !== null && value !== undefined;
338 | }
339 |
340 | export interface MessageFns {
341 | encode(message: T, writer?: BinaryWriter): BinaryWriter;
342 | decode(input: BinaryReader | Uint8Array, length?: number): T;
343 | fromJSON(object: any): T;
344 | toJSON(message: T): unknown;
345 | create, I>>(base?: I): T;
346 | fromPartial, I>>(object: I): T;
347 | }
348 |
--------------------------------------------------------------------------------
/src/proto/tm2/tx.ts:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-ts_proto. DO NOT EDIT.
2 | // versions:
3 | // protoc-gen-ts_proto v2.2.0
4 | // protoc v5.29.0
5 | // source: tm2/tx.proto
6 |
7 | /* eslint-disable */
8 | import { BinaryReader, BinaryWriter } from '@bufbuild/protobuf/wire';
9 | import Long from 'long';
10 | import { Any } from '../google/protobuf/any';
11 |
12 | export const protobufPackage = 'tm2.tx';
13 |
14 | export interface Tx {
15 | /** specific message types */
16 | messages: Any[];
17 | /** transaction costs (fee) */
18 | fee?: TxFee | undefined;
19 | /** the signatures for the transaction */
20 | signatures: TxSignature[];
21 | /** memo attached to the transaction */
22 | memo: string;
23 | }
24 |
25 | export interface TxFee {
26 | /** gas limit */
27 | gasWanted: Long;
28 | /** gas fee details () */
29 | gasFee: string;
30 | }
31 |
32 | export interface TxSignature {
33 | /** public key associated with the signature */
34 | pubKey?: Any | undefined;
35 | /** the signature */
36 | signature: Uint8Array;
37 | }
38 |
39 | export interface PubKeySecp256k1 {
40 | key: Uint8Array;
41 | }
42 |
43 | function createBaseTx(): Tx {
44 | return { messages: [], fee: undefined, signatures: [], memo: '' };
45 | }
46 |
47 | export const Tx: MessageFns = {
48 | encode(message: Tx, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
49 | for (const v of message.messages) {
50 | Any.encode(v!, writer.uint32(10).fork()).join();
51 | }
52 | if (message.fee !== undefined) {
53 | TxFee.encode(message.fee, writer.uint32(18).fork()).join();
54 | }
55 | for (const v of message.signatures) {
56 | TxSignature.encode(v!, writer.uint32(26).fork()).join();
57 | }
58 | if (message.memo !== '') {
59 | writer.uint32(34).string(message.memo);
60 | }
61 | return writer;
62 | },
63 |
64 | decode(input: BinaryReader | Uint8Array, length?: number): Tx {
65 | const reader =
66 | input instanceof BinaryReader ? input : new BinaryReader(input);
67 | let end = length === undefined ? reader.len : reader.pos + length;
68 | const message = createBaseTx();
69 | while (reader.pos < end) {
70 | const tag = reader.uint32();
71 | switch (tag >>> 3) {
72 | case 1:
73 | if (tag !== 10) {
74 | break;
75 | }
76 |
77 | message.messages.push(Any.decode(reader, reader.uint32()));
78 | continue;
79 | case 2:
80 | if (tag !== 18) {
81 | break;
82 | }
83 |
84 | message.fee = TxFee.decode(reader, reader.uint32());
85 | continue;
86 | case 3:
87 | if (tag !== 26) {
88 | break;
89 | }
90 |
91 | message.signatures.push(TxSignature.decode(reader, reader.uint32()));
92 | continue;
93 | case 4:
94 | if (tag !== 34) {
95 | break;
96 | }
97 |
98 | message.memo = reader.string();
99 | continue;
100 | }
101 | if ((tag & 7) === 4 || tag === 0) {
102 | break;
103 | }
104 | reader.skip(tag & 7);
105 | }
106 | return message;
107 | },
108 |
109 | fromJSON(object: any): Tx {
110 | return {
111 | messages: globalThis.Array.isArray(object?.messages)
112 | ? object.messages.map((e: any) => Any.fromJSON(e))
113 | : [],
114 | fee: isSet(object.fee) ? TxFee.fromJSON(object.fee) : undefined,
115 | signatures: globalThis.Array.isArray(object?.signatures)
116 | ? object.signatures.map((e: any) => TxSignature.fromJSON(e))
117 | : [],
118 | memo: isSet(object.memo) ? globalThis.String(object.memo) : '',
119 | };
120 | },
121 |
122 | toJSON(message: Tx): unknown {
123 | const obj: any = {};
124 | if (message.messages?.length) {
125 | obj.messages = message.messages.map((e) => Any.toJSON(e));
126 | }
127 | if (message.fee !== undefined) {
128 | obj.fee = TxFee.toJSON(message.fee);
129 | }
130 | if (message.signatures?.length) {
131 | obj.signatures = message.signatures.map((e) => TxSignature.toJSON(e));
132 | }
133 | if (message.memo !== undefined) {
134 | obj.memo = message.memo;
135 | }
136 | return obj;
137 | },
138 |
139 | create, I>>(base?: I): Tx {
140 | return Tx.fromPartial(base ?? ({} as any));
141 | },
142 | fromPartial, I>>(object: I): Tx {
143 | const message = createBaseTx();
144 | message.messages = object.messages?.map((e) => Any.fromPartial(e)) || [];
145 | message.fee =
146 | object.fee !== undefined && object.fee !== null
147 | ? TxFee.fromPartial(object.fee)
148 | : undefined;
149 | message.signatures =
150 | object.signatures?.map((e) => TxSignature.fromPartial(e)) || [];
151 | message.memo = object.memo ?? '';
152 | return message;
153 | },
154 | };
155 |
156 | function createBaseTxFee(): TxFee {
157 | return { gasWanted: Long.ZERO, gasFee: '' };
158 | }
159 |
160 | export const TxFee: MessageFns = {
161 | encode(
162 | message: TxFee,
163 | writer: BinaryWriter = new BinaryWriter()
164 | ): BinaryWriter {
165 | if (!message.gasWanted.equals(Long.ZERO)) {
166 | writer.uint32(8).sint64(message.gasWanted.toString());
167 | }
168 | if (message.gasFee !== '') {
169 | writer.uint32(18).string(message.gasFee);
170 | }
171 | return writer;
172 | },
173 |
174 | decode(input: BinaryReader | Uint8Array, length?: number): TxFee {
175 | const reader =
176 | input instanceof BinaryReader ? input : new BinaryReader(input);
177 | let end = length === undefined ? reader.len : reader.pos + length;
178 | const message = createBaseTxFee();
179 | while (reader.pos < end) {
180 | const tag = reader.uint32();
181 | switch (tag >>> 3) {
182 | case 1:
183 | if (tag !== 8) {
184 | break;
185 | }
186 |
187 | message.gasWanted = Long.fromString(reader.sint64().toString());
188 | continue;
189 | case 2:
190 | if (tag !== 18) {
191 | break;
192 | }
193 |
194 | message.gasFee = reader.string();
195 | continue;
196 | }
197 | if ((tag & 7) === 4 || tag === 0) {
198 | break;
199 | }
200 | reader.skip(tag & 7);
201 | }
202 | return message;
203 | },
204 |
205 | fromJSON(object: any): TxFee {
206 | return {
207 | gasWanted: isSet(object.gasWanted)
208 | ? Long.fromValue(object.gasWanted)
209 | : Long.ZERO,
210 | gasFee: isSet(object.gasFee) ? globalThis.String(object.gasFee) : '',
211 | };
212 | },
213 |
214 | toJSON(message: TxFee): unknown {
215 | const obj: any = {};
216 | if (message.gasWanted !== undefined) {
217 | obj.gasWanted = (message.gasWanted || Long.ZERO).toString();
218 | }
219 | if (message.gasFee !== undefined) {
220 | obj.gasFee = message.gasFee;
221 | }
222 | return obj;
223 | },
224 |
225 | create, I>>(base?: I): TxFee {
226 | return TxFee.fromPartial(base ?? ({} as any));
227 | },
228 | fromPartial, I>>(object: I): TxFee {
229 | const message = createBaseTxFee();
230 | message.gasWanted =
231 | object.gasWanted !== undefined && object.gasWanted !== null
232 | ? Long.fromValue(object.gasWanted)
233 | : Long.ZERO;
234 | message.gasFee = object.gasFee ?? '';
235 | return message;
236 | },
237 | };
238 |
239 | function createBaseTxSignature(): TxSignature {
240 | return { pubKey: undefined, signature: new Uint8Array(0) };
241 | }
242 |
243 | export const TxSignature: MessageFns = {
244 | encode(
245 | message: TxSignature,
246 | writer: BinaryWriter = new BinaryWriter()
247 | ): BinaryWriter {
248 | if (message.pubKey !== undefined) {
249 | Any.encode(message.pubKey, writer.uint32(10).fork()).join();
250 | }
251 | if (message.signature.length !== 0) {
252 | writer.uint32(18).bytes(message.signature);
253 | }
254 | return writer;
255 | },
256 |
257 | decode(input: BinaryReader | Uint8Array, length?: number): TxSignature {
258 | const reader =
259 | input instanceof BinaryReader ? input : new BinaryReader(input);
260 | let end = length === undefined ? reader.len : reader.pos + length;
261 | const message = createBaseTxSignature();
262 | while (reader.pos < end) {
263 | const tag = reader.uint32();
264 | switch (tag >>> 3) {
265 | case 1:
266 | if (tag !== 10) {
267 | break;
268 | }
269 |
270 | message.pubKey = Any.decode(reader, reader.uint32());
271 | continue;
272 | case 2:
273 | if (tag !== 18) {
274 | break;
275 | }
276 |
277 | message.signature = reader.bytes();
278 | continue;
279 | }
280 | if ((tag & 7) === 4 || tag === 0) {
281 | break;
282 | }
283 | reader.skip(tag & 7);
284 | }
285 | return message;
286 | },
287 |
288 | fromJSON(object: any): TxSignature {
289 | return {
290 | pubKey: isSet(object.pubKey) ? Any.fromJSON(object.pubKey) : undefined,
291 | signature: isSet(object.signature)
292 | ? bytesFromBase64(object.signature)
293 | : new Uint8Array(0),
294 | };
295 | },
296 |
297 | toJSON(message: TxSignature): unknown {
298 | const obj: any = {};
299 | if (message.pubKey !== undefined) {
300 | obj.pubKey = Any.toJSON(message.pubKey);
301 | }
302 | if (message.signature !== undefined) {
303 | obj.signature = base64FromBytes(message.signature);
304 | }
305 | return obj;
306 | },
307 |
308 | create, I>>(base?: I): TxSignature {
309 | return TxSignature.fromPartial(base ?? ({} as any));
310 | },
311 | fromPartial, I>>(
312 | object: I
313 | ): TxSignature {
314 | const message = createBaseTxSignature();
315 | message.pubKey =
316 | object.pubKey !== undefined && object.pubKey !== null
317 | ? Any.fromPartial(object.pubKey)
318 | : undefined;
319 | message.signature = object.signature ?? new Uint8Array(0);
320 | return message;
321 | },
322 | };
323 |
324 | function createBasePubKeySecp256k1(): PubKeySecp256k1 {
325 | return { key: new Uint8Array(0) };
326 | }
327 |
328 | export const PubKeySecp256k1: MessageFns = {
329 | encode(
330 | message: PubKeySecp256k1,
331 | writer: BinaryWriter = new BinaryWriter()
332 | ): BinaryWriter {
333 | if (message.key.length !== 0) {
334 | writer.uint32(10).bytes(message.key);
335 | }
336 | return writer;
337 | },
338 |
339 | decode(input: BinaryReader | Uint8Array, length?: number): PubKeySecp256k1 {
340 | const reader =
341 | input instanceof BinaryReader ? input : new BinaryReader(input);
342 | let end = length === undefined ? reader.len : reader.pos + length;
343 | const message = createBasePubKeySecp256k1();
344 | while (reader.pos < end) {
345 | const tag = reader.uint32();
346 | switch (tag >>> 3) {
347 | case 1:
348 | if (tag !== 10) {
349 | break;
350 | }
351 |
352 | message.key = reader.bytes();
353 | continue;
354 | }
355 | if ((tag & 7) === 4 || tag === 0) {
356 | break;
357 | }
358 | reader.skip(tag & 7);
359 | }
360 | return message;
361 | },
362 |
363 | fromJSON(object: any): PubKeySecp256k1 {
364 | return {
365 | key: isSet(object.key) ? bytesFromBase64(object.key) : new Uint8Array(0),
366 | };
367 | },
368 |
369 | toJSON(message: PubKeySecp256k1): unknown {
370 | const obj: any = {};
371 | if (message.key !== undefined) {
372 | obj.key = base64FromBytes(message.key);
373 | }
374 | return obj;
375 | },
376 |
377 | create, I>>(
378 | base?: I
379 | ): PubKeySecp256k1 {
380 | return PubKeySecp256k1.fromPartial(base ?? ({} as any));
381 | },
382 | fromPartial, I>>(
383 | object: I
384 | ): PubKeySecp256k1 {
385 | const message = createBasePubKeySecp256k1();
386 | message.key = object.key ?? new Uint8Array(0);
387 | return message;
388 | },
389 | };
390 |
391 | function bytesFromBase64(b64: string): Uint8Array {
392 | if ((globalThis as any).Buffer) {
393 | return Uint8Array.from(globalThis.Buffer.from(b64, 'base64'));
394 | } else {
395 | const bin = globalThis.atob(b64);
396 | const arr = new Uint8Array(bin.length);
397 | for (let i = 0; i < bin.length; ++i) {
398 | arr[i] = bin.charCodeAt(i);
399 | }
400 | return arr;
401 | }
402 | }
403 |
404 | function base64FromBytes(arr: Uint8Array): string {
405 | if ((globalThis as any).Buffer) {
406 | return globalThis.Buffer.from(arr).toString('base64');
407 | } else {
408 | const bin: string[] = [];
409 | arr.forEach((byte) => {
410 | bin.push(globalThis.String.fromCharCode(byte));
411 | });
412 | return globalThis.btoa(bin.join(''));
413 | }
414 | }
415 |
416 | type Builtin =
417 | | Date
418 | | Function
419 | | Uint8Array
420 | | string
421 | | number
422 | | boolean
423 | | undefined;
424 |
425 | export type DeepPartial = T extends Builtin
426 | ? T
427 | : T extends Long
428 | ? string | number | Long
429 | : T extends globalThis.Array
430 | ? globalThis.Array>
431 | : T extends ReadonlyArray
432 | ? ReadonlyArray>
433 | : T extends {}
434 | ? { [K in keyof T]?: DeepPartial }
435 | : Partial;
436 |
437 | type KeysOfUnion = T extends T ? keyof T : never;
438 | export type Exact = P extends Builtin
439 | ? P
440 | : P & { [K in keyof P]: Exact
} & {
441 | [K in Exclude>]: never;
442 | };
443 |
444 | function isSet(value: any): boolean {
445 | return value !== null && value !== undefined;
446 | }
447 |
448 | export interface MessageFns {
449 | encode(message: T, writer?: BinaryWriter): BinaryWriter;
450 | decode(input: BinaryReader | Uint8Array, length?: number): T;
451 | fromJSON(object: any): T;
452 | toJSON(message: T): unknown;
453 | create, I>>(base?: I): T;
454 | fromPartial, I>>(object: I): T;
455 | }
456 |
--------------------------------------------------------------------------------
/src/provider/endpoints.ts:
--------------------------------------------------------------------------------
1 | export enum CommonEndpoint {
2 | HEALTH = 'health',
3 | STATUS = 'status',
4 | }
5 |
6 | export enum ConsensusEndpoint {
7 | NET_INFO = 'net_info',
8 | GENESIS = 'genesis',
9 | CONSENSUS_PARAMS = 'consensus_params',
10 | CONSENSUS_STATE = 'consensus_state',
11 | COMMIT = 'commit',
12 | VALIDATORS = 'validators',
13 | }
14 |
15 | export enum BlockEndpoint {
16 | BLOCK = 'block',
17 | BLOCK_RESULTS = 'block_results',
18 | BLOCKCHAIN = 'blockchain',
19 | }
20 |
21 | export enum TransactionEndpoint {
22 | NUM_UNCONFIRMED_TXS = 'num_unconfirmed_txs',
23 | UNCONFIRMED_TXS = 'unconfirmed_txs',
24 | BROADCAST_TX_ASYNC = 'broadcast_tx_async',
25 | BROADCAST_TX_SYNC = 'broadcast_tx_sync',
26 | BROADCAST_TX_COMMIT = 'broadcast_tx_commit',
27 | TX = 'tx',
28 | }
29 |
30 | export enum ABCIEndpoint {
31 | ABCI_INFO = 'abci_info',
32 | ABCI_QUERY = 'abci_query',
33 | }
34 |
--------------------------------------------------------------------------------
/src/provider/errors/errors.ts:
--------------------------------------------------------------------------------
1 | import {
2 | GasOverflowErrorMessage,
3 | InsufficientCoinsErrorMessage,
4 | InsufficientFeeErrorMessage,
5 | InsufficientFundsErrorMessage,
6 | InternalErrorMessage,
7 | InvalidAddressErrorMessage,
8 | InvalidCoinsErrorMessage,
9 | InvalidGasWantedErrorMessage,
10 | InvalidPubKeyErrorMessage,
11 | InvalidSequenceErrorMessage,
12 | MemoTooLargeErrorMessage,
13 | NoSignaturesErrorMessage,
14 | OutOfGasErrorMessage,
15 | TooManySignaturesErrorMessage,
16 | TxDecodeErrorMessage,
17 | UnauthorizedErrorMessage,
18 | UnknownAddressErrorMessage,
19 | UnknownRequestErrorMessage,
20 | } from './messages';
21 |
22 | class TM2Error extends Error {
23 | public log?: string;
24 |
25 | constructor(message: string, log?: string) {
26 | super(message);
27 |
28 | this.log = log;
29 | }
30 | }
31 |
32 | class InternalError extends TM2Error {
33 | constructor(log?: string) {
34 | super(InternalErrorMessage, log);
35 | }
36 | }
37 |
38 | class TxDecodeError extends TM2Error {
39 | constructor(log?: string) {
40 | super(TxDecodeErrorMessage, log);
41 | }
42 | }
43 |
44 | class InvalidSequenceError extends TM2Error {
45 | constructor(log?: string) {
46 | super(InvalidSequenceErrorMessage, log);
47 | }
48 | }
49 |
50 | class UnauthorizedError extends TM2Error {
51 | constructor(log?: string) {
52 | super(UnauthorizedErrorMessage, log);
53 | }
54 | }
55 |
56 | class InsufficientFundsError extends TM2Error {
57 | constructor(log?: string) {
58 | super(InsufficientFundsErrorMessage, log);
59 | }
60 | }
61 |
62 | class UnknownRequestError extends TM2Error {
63 | constructor(log?: string) {
64 | super(UnknownRequestErrorMessage, log);
65 | }
66 | }
67 |
68 | class InvalidAddressError extends TM2Error {
69 | constructor(log?: string) {
70 | super(InvalidAddressErrorMessage, log);
71 | }
72 | }
73 |
74 | class UnknownAddressError extends TM2Error {
75 | constructor(log?: string) {
76 | super(UnknownAddressErrorMessage, log);
77 | }
78 | }
79 |
80 | class InvalidPubKeyError extends TM2Error {
81 | constructor(log?: string) {
82 | super(InvalidPubKeyErrorMessage, log);
83 | }
84 | }
85 |
86 | class InsufficientCoinsError extends TM2Error {
87 | constructor(log?: string) {
88 | super(InsufficientCoinsErrorMessage, log);
89 | }
90 | }
91 |
92 | class InvalidCoinsError extends TM2Error {
93 | constructor(log?: string) {
94 | super(InvalidCoinsErrorMessage, log);
95 | }
96 | }
97 |
98 | class InvalidGasWantedError extends TM2Error {
99 | constructor(log?: string) {
100 | super(InvalidGasWantedErrorMessage, log);
101 | }
102 | }
103 |
104 | class OutOfGasError extends TM2Error {
105 | constructor(log?: string) {
106 | super(OutOfGasErrorMessage, log);
107 | }
108 | }
109 |
110 | class MemoTooLargeError extends TM2Error {
111 | constructor(log?: string) {
112 | super(MemoTooLargeErrorMessage, log);
113 | }
114 | }
115 |
116 | class InsufficientFeeError extends TM2Error {
117 | constructor(log?: string) {
118 | super(InsufficientFeeErrorMessage, log);
119 | }
120 | }
121 |
122 | class TooManySignaturesError extends TM2Error {
123 | constructor(log?: string) {
124 | super(TooManySignaturesErrorMessage, log);
125 | }
126 | }
127 |
128 | class NoSignaturesError extends TM2Error {
129 | constructor(log?: string) {
130 | super(NoSignaturesErrorMessage, log);
131 | }
132 | }
133 |
134 | class GasOverflowError extends TM2Error {
135 | constructor(log?: string) {
136 | super(GasOverflowErrorMessage, log);
137 | }
138 | }
139 |
140 | export {
141 | TM2Error,
142 | InternalError,
143 | TxDecodeError,
144 | InvalidSequenceError,
145 | UnauthorizedError,
146 | InsufficientFundsError,
147 | UnknownRequestError,
148 | InvalidAddressError,
149 | UnknownAddressError,
150 | InvalidPubKeyError,
151 | InsufficientCoinsError,
152 | InvalidCoinsError,
153 | InvalidGasWantedError,
154 | OutOfGasError,
155 | MemoTooLargeError,
156 | InsufficientFeeError,
157 | TooManySignaturesError,
158 | NoSignaturesError,
159 | GasOverflowError,
160 | };
161 |
--------------------------------------------------------------------------------
/src/provider/errors/index.ts:
--------------------------------------------------------------------------------
1 | export * from './errors';
2 |
--------------------------------------------------------------------------------
/src/provider/errors/messages.ts:
--------------------------------------------------------------------------------
1 | // Errors constructed from:
2 | // https://github.com/gnolang/gno/blob/master/tm2/pkg/std/errors.go
3 |
4 | const InternalErrorMessage = 'internal error encountered';
5 | const TxDecodeErrorMessage = 'unable to decode tx';
6 | const InvalidSequenceErrorMessage = 'invalid sequence';
7 | const UnauthorizedErrorMessage = 'signature is unauthorized';
8 | const InsufficientFundsErrorMessage = 'insufficient funds';
9 | const UnknownRequestErrorMessage = 'unknown request';
10 | const InvalidAddressErrorMessage = 'invalid address';
11 | const UnknownAddressErrorMessage = 'unknown address';
12 | const InvalidPubKeyErrorMessage = 'invalid pubkey';
13 | const InsufficientCoinsErrorMessage = 'insufficient coins';
14 | const InvalidCoinsErrorMessage = 'invalid coins';
15 | const InvalidGasWantedErrorMessage = 'invalid gas wanted';
16 | const OutOfGasErrorMessage = 'out of gas';
17 | const MemoTooLargeErrorMessage = 'memo too large';
18 | const InsufficientFeeErrorMessage = 'insufficient fee';
19 | const TooManySignaturesErrorMessage = 'too many signatures';
20 | const NoSignaturesErrorMessage = 'no signatures';
21 | const GasOverflowErrorMessage = 'gas overflow';
22 |
23 | export {
24 | InternalErrorMessage,
25 | TxDecodeErrorMessage,
26 | InvalidSequenceErrorMessage,
27 | UnauthorizedErrorMessage,
28 | InsufficientFundsErrorMessage,
29 | UnknownRequestErrorMessage,
30 | InvalidAddressErrorMessage,
31 | UnknownAddressErrorMessage,
32 | InvalidPubKeyErrorMessage,
33 | InsufficientCoinsErrorMessage,
34 | InvalidCoinsErrorMessage,
35 | InvalidGasWantedErrorMessage,
36 | OutOfGasErrorMessage,
37 | MemoTooLargeErrorMessage,
38 | InsufficientFeeErrorMessage,
39 | TooManySignaturesErrorMessage,
40 | NoSignaturesErrorMessage,
41 | GasOverflowErrorMessage,
42 | };
43 |
--------------------------------------------------------------------------------
/src/provider/index.ts:
--------------------------------------------------------------------------------
1 | export * from './jsonrpc';
2 | export * from './types';
3 | export * from './utility';
4 | export * from './websocket';
5 | export * from './endpoints';
6 | export * from './provider';
7 | export * from './errors';
8 |
--------------------------------------------------------------------------------
/src/provider/jsonrpc/index.ts:
--------------------------------------------------------------------------------
1 | export * from './jsonrpc';
2 |
--------------------------------------------------------------------------------
/src/provider/jsonrpc/jsonrpc.test.ts:
--------------------------------------------------------------------------------
1 | import { sha256 } from '@cosmjs/crypto';
2 | import axios from 'axios';
3 | import { mock } from 'jest-mock-extended';
4 | import Long from 'long';
5 | import { Tx } from '../../proto';
6 | import { CommonEndpoint, TransactionEndpoint } from '../endpoints';
7 | import { TM2Error } from '../errors';
8 | import { UnauthorizedErrorMessage } from '../errors/messages';
9 | import {
10 | ABCIAccount,
11 | ABCIErrorKey,
12 | ABCIResponse,
13 | BlockInfo,
14 | BlockResult,
15 | BroadcastTxSyncResult,
16 | ConsensusParams,
17 | NetworkInfo,
18 | RPCRequest,
19 | Status,
20 | } from '../types';
21 | import { newResponse, stringToBase64, uint8ArrayToBase64 } from '../utility';
22 | import { JSONRPCProvider } from './jsonrpc';
23 |
24 | jest.mock('axios');
25 |
26 | const mockedAxios = axios as jest.Mocked;
27 | const mockURL = '127.0.0.1:26657';
28 |
29 | describe('JSON-RPC Provider', () => {
30 | test('estimateGas', async () => {
31 | const tx = Tx.fromJSON({
32 | signatures: [],
33 | fee: {
34 | gasFee: '',
35 | gasWanted: new Long(0),
36 | },
37 | messages: [],
38 | memo: '',
39 | });
40 | const expectedEstimation = 44900;
41 |
42 | const mockABCIResponse: ABCIResponse = mock();
43 | mockABCIResponse.response.Value =
44 | 'CiMiIW1zZzowLHN1Y2Nlc3M6dHJ1ZSxsb2c6LGV2ZW50czpbXRCAiXoYyL0F';
45 |
46 | mockedAxios.post.mockResolvedValue({
47 | data: newResponse(mockABCIResponse),
48 | });
49 |
50 | // Create the provider
51 | const provider = new JSONRPCProvider(mockURL);
52 | const estimation = await provider.estimateGas(tx);
53 |
54 | expect(axios.post).toHaveBeenCalled();
55 | expect(estimation).toEqual(expectedEstimation);
56 | });
57 |
58 | test('getNetwork', async () => {
59 | const mockInfo: NetworkInfo = mock();
60 | mockInfo.listening = false;
61 |
62 | mockedAxios.post.mockResolvedValue({
63 | data: newResponse(mockInfo),
64 | });
65 |
66 | // Create the provider
67 | const provider = new JSONRPCProvider(mockURL);
68 | const info = await provider.getNetwork();
69 |
70 | expect(axios.post).toHaveBeenCalled();
71 | expect(info).toEqual(mockInfo);
72 | });
73 |
74 | test('getBlock', async () => {
75 | const mockInfo: BlockInfo = mock();
76 |
77 | mockedAxios.post.mockResolvedValue({
78 | data: newResponse(mockInfo),
79 | });
80 |
81 | // Create the provider
82 | const provider = new JSONRPCProvider(mockURL);
83 | const info = await provider.getBlock(0);
84 |
85 | expect(axios.post).toHaveBeenCalled();
86 | expect(info).toEqual(mockInfo);
87 | });
88 |
89 | test('getBlockResult', async () => {
90 | const mockResult: BlockResult = mock();
91 |
92 | mockedAxios.post.mockResolvedValue({
93 | data: newResponse(mockResult),
94 | });
95 |
96 | // Create the provider
97 | const provider = new JSONRPCProvider(mockURL);
98 | const result = await provider.getBlockResult(0);
99 |
100 | expect(axios.post).toHaveBeenCalled();
101 | expect(result).toEqual(mockResult);
102 | });
103 |
104 | describe('sendTransaction', () => {
105 | const validResult: BroadcastTxSyncResult = {
106 | error: null,
107 | data: null,
108 | Log: '',
109 | hash: 'hash123',
110 | };
111 |
112 | const mockError = '/std.UnauthorizedError';
113 | const mockLog = 'random error message';
114 | const invalidResult: BroadcastTxSyncResult = {
115 | error: {
116 | [ABCIErrorKey]: mockError,
117 | },
118 | data: null,
119 | Log: mockLog,
120 | hash: '',
121 | };
122 |
123 | test.each([
124 | [validResult, validResult.hash, '', ''], // no error
125 | [invalidResult, invalidResult.hash, UnauthorizedErrorMessage, mockLog], // error out
126 | ])('case %#', async (response, expectedHash, expectedErr, expectedLog) => {
127 | mockedAxios.post.mockResolvedValue({
128 | data: newResponse(response),
129 | });
130 |
131 | try {
132 | // Create the provider
133 | const provider = new JSONRPCProvider(mockURL);
134 | const tx = await provider.sendTransaction(
135 | 'encoded tx',
136 | TransactionEndpoint.BROADCAST_TX_SYNC
137 | );
138 |
139 | expect(axios.post).toHaveBeenCalled();
140 | expect(tx.hash).toEqual(expectedHash);
141 |
142 | if (expectedErr != '') {
143 | fail('expected error');
144 | }
145 | } catch (e) {
146 | expect((e as Error).message).toBe(expectedErr);
147 | expect((e as TM2Error).log).toBe(expectedLog);
148 | }
149 | });
150 | });
151 |
152 | test('waitForTransaction', async () => {
153 | const emptyBlock: BlockInfo = mock();
154 | emptyBlock.block.data = {
155 | txs: [],
156 | };
157 |
158 | const tx: Tx = {
159 | messages: [],
160 | signatures: [],
161 | memo: 'tx memo',
162 | };
163 |
164 | const encodedTx = Tx.encode(tx).finish();
165 | const txHash = sha256(encodedTx);
166 |
167 | const filledBlock: BlockInfo = mock();
168 | filledBlock.block.data = {
169 | txs: [uint8ArrayToBase64(encodedTx)],
170 | };
171 |
172 | const latestBlock = 5;
173 | const startBlock = latestBlock - 2;
174 |
175 | const mockStatus: Status = mock();
176 | mockStatus.sync_info.latest_block_height = `${latestBlock}`;
177 |
178 | const responseMap: Map = new Map([
179 | [latestBlock, filledBlock],
180 | [latestBlock - 1, emptyBlock],
181 | [startBlock, emptyBlock],
182 | ]);
183 |
184 | mockedAxios.post.mockImplementation((url, params, config): Promise => {
185 | const request = params as RPCRequest;
186 |
187 | if (request.method == CommonEndpoint.STATUS) {
188 | return Promise.resolve({
189 | data: newResponse(mockStatus),
190 | });
191 | }
192 |
193 | if (!request.params) {
194 | return Promise.reject('invalid params');
195 | }
196 |
197 | const blockNum: number = +(request.params[0] as string[]);
198 | const info = responseMap.get(blockNum);
199 |
200 | return Promise.resolve({
201 | data: newResponse(info),
202 | });
203 | });
204 |
205 | // Create the provider
206 | const provider = new JSONRPCProvider(mockURL);
207 | const receivedTx = await provider.waitForTransaction(
208 | uint8ArrayToBase64(txHash),
209 | startBlock
210 | );
211 |
212 | expect(axios.post).toHaveBeenCalled();
213 | expect(receivedTx).toEqual(tx);
214 | });
215 |
216 | test('getConsensusParams', async () => {
217 | const mockParams: ConsensusParams = mock();
218 | mockParams.block_height = '1';
219 |
220 | mockedAxios.post.mockResolvedValue({
221 | data: newResponse(mockParams),
222 | });
223 |
224 | // Create the provider
225 | const provider = new JSONRPCProvider(mockURL);
226 | const params = await provider.getConsensusParams(1);
227 |
228 | expect(axios.post).toHaveBeenCalled();
229 | expect(params).toEqual(mockParams);
230 | });
231 |
232 | test('getStatus', async () => {
233 | const mockStatus: Status = mock();
234 | mockStatus.validator_info.address = 'address';
235 |
236 | mockedAxios.post.mockResolvedValue({
237 | data: newResponse(mockStatus),
238 | });
239 |
240 | // Create the provider
241 | const provider = new JSONRPCProvider(mockURL);
242 | const status = await provider.getStatus();
243 |
244 | expect(axios.post).toHaveBeenCalled();
245 | expect(status).toEqual(mockStatus);
246 | });
247 |
248 | test('getBlockNumber', async () => {
249 | const expectedBlockNumber = 10;
250 | const mockStatus: Status = mock();
251 | mockStatus.sync_info.latest_block_height = `${expectedBlockNumber}`;
252 |
253 | mockedAxios.post.mockResolvedValue({
254 | data: newResponse(mockStatus),
255 | });
256 |
257 | // Create the provider
258 | const provider = new JSONRPCProvider(mockURL);
259 | const blockNumber = await provider.getBlockNumber();
260 |
261 | expect(axios.post).toHaveBeenCalled();
262 | expect(blockNumber).toEqual(expectedBlockNumber);
263 | });
264 |
265 | describe('getBalance', () => {
266 | const denomination = 'atom';
267 | test.each([
268 | ['"5gnot,100atom"', 100], // balance found
269 | ['"5universe"', 0], // balance not found
270 | ['""', 0], // account doesn't exist
271 | ])('case %#', async (existing, expected) => {
272 | const mockABCIResponse: ABCIResponse = mock();
273 | mockABCIResponse.response.ResponseBase = {
274 | Log: '',
275 | Info: '',
276 | Data: stringToBase64(existing),
277 | Error: null,
278 | Events: null,
279 | };
280 |
281 | mockedAxios.post.mockResolvedValue({
282 | data: newResponse(mockABCIResponse),
283 | });
284 |
285 | // Create the provider
286 | const provider = new JSONRPCProvider(mockURL);
287 | const balance = await provider.getBalance('address', denomination);
288 |
289 | expect(axios.post).toHaveBeenCalled();
290 | expect(balance).toBe(expected);
291 | });
292 | });
293 |
294 | describe('getSequence', () => {
295 | const validAccount: ABCIAccount = {
296 | BaseAccount: {
297 | address: 'random address',
298 | coins: '',
299 | public_key: null,
300 | account_number: '0',
301 | sequence: '10',
302 | },
303 | };
304 |
305 | test.each([
306 | [
307 | JSON.stringify(validAccount),
308 | parseInt(validAccount.BaseAccount.sequence, 10),
309 | ], // account exists
310 | ['null', 0], // account doesn't exist
311 | ])('case %#', async (response, expected) => {
312 | const mockABCIResponse: ABCIResponse = mock();
313 | mockABCIResponse.response.ResponseBase = {
314 | Log: '',
315 | Info: '',
316 | Data: stringToBase64(response),
317 | Error: null,
318 | Events: null,
319 | };
320 |
321 | mockedAxios.post.mockResolvedValue({
322 | data: newResponse(mockABCIResponse),
323 | });
324 |
325 | // Create the provider
326 | const provider = new JSONRPCProvider(mockURL);
327 | const sequence = await provider.getAccountSequence('address');
328 |
329 | expect(axios.post).toHaveBeenCalled();
330 | expect(sequence).toBe(expected);
331 | });
332 | });
333 |
334 | describe('getAccountNumber', () => {
335 | const validAccount: ABCIAccount = {
336 | BaseAccount: {
337 | address: 'random address',
338 | coins: '',
339 | public_key: null,
340 | account_number: '10',
341 | sequence: '0',
342 | },
343 | };
344 |
345 | test.each([
346 | [
347 | JSON.stringify(validAccount),
348 | parseInt(validAccount.BaseAccount.account_number, 10),
349 | ], // account exists
350 | ['null', 0], // account doesn't exist
351 | ])('case %#', async (response, expected) => {
352 | const mockABCIResponse: ABCIResponse = mock();
353 | mockABCIResponse.response.ResponseBase = {
354 | Log: '',
355 | Info: '',
356 | Data: stringToBase64(response),
357 | Error: null,
358 | Events: null,
359 | };
360 |
361 | mockedAxios.post.mockResolvedValue({
362 | data: newResponse(mockABCIResponse),
363 | });
364 |
365 | try {
366 | // Create the provider
367 | const provider = new JSONRPCProvider(mockURL);
368 | const accountNumber = await provider.getAccountNumber('address');
369 |
370 | expect(axios.post).toHaveBeenCalled();
371 | expect(accountNumber).toBe(expected);
372 | } catch (e) {
373 | expect((e as Error).message).toContain('account is not initialized');
374 | }
375 | });
376 | });
377 | });
378 |
--------------------------------------------------------------------------------
/src/provider/jsonrpc/jsonrpc.ts:
--------------------------------------------------------------------------------
1 | import { Tx } from '../../proto';
2 | import { RestService } from '../../services';
3 | import {
4 | ABCIEndpoint,
5 | BlockEndpoint,
6 | CommonEndpoint,
7 | ConsensusEndpoint,
8 | TransactionEndpoint,
9 | } from '../endpoints';
10 | import { Provider } from '../provider';
11 | import {
12 | ABCIErrorKey,
13 | ABCIResponse,
14 | BlockInfo,
15 | BlockResult,
16 | BroadcastTransactionMap,
17 | BroadcastTxCommitResult,
18 | BroadcastTxSyncResult,
19 | ConsensusParams,
20 | NetworkInfo,
21 | RPCRequest,
22 | Status,
23 | TxResult,
24 | } from '../types';
25 | import {
26 | extractAccountNumberFromResponse,
27 | extractBalanceFromResponse,
28 | extractSequenceFromResponse,
29 | extractSimulateFromResponse,
30 | newRequest,
31 | uint8ArrayToBase64,
32 | waitForTransaction,
33 | } from '../utility';
34 | import { constructRequestError } from '../utility/errors.utility';
35 |
36 | /**
37 | * Provider based on JSON-RPC HTTP requests
38 | */
39 | export class JSONRPCProvider implements Provider {
40 | protected readonly baseURL: string;
41 |
42 | /**
43 | * Creates a new instance of the JSON-RPC Provider
44 | * @param {string} baseURL the JSON-RPC URL of the node
45 | */
46 | constructor(baseURL: string) {
47 | this.baseURL = baseURL;
48 | }
49 |
50 | async estimateGas(tx: Tx): Promise {
51 | const encodedTx = uint8ArrayToBase64(Tx.encode(tx).finish());
52 | const abciResponse: ABCIResponse = await RestService.post(
53 | this.baseURL,
54 | {
55 | request: newRequest(ABCIEndpoint.ABCI_QUERY, [
56 | `.app/simulate`,
57 | `${encodedTx}`,
58 | '0', // Height; not supported > 0 for now
59 | false,
60 | ]),
61 | }
62 | );
63 |
64 | const simulateResult = extractSimulateFromResponse(abciResponse);
65 |
66 | return simulateResult.gasUsed.toInt();
67 | }
68 |
69 | async getBalance(
70 | address: string,
71 | denomination?: string,
72 | height?: number
73 | ): Promise {
74 | const abciResponse: ABCIResponse = await RestService.post(
75 | this.baseURL,
76 | {
77 | request: newRequest(ABCIEndpoint.ABCI_QUERY, [
78 | `bank/balances/${address}`,
79 | '',
80 | '0', // Height; not supported > 0 for now
81 | false,
82 | ]),
83 | }
84 | );
85 |
86 | return extractBalanceFromResponse(
87 | abciResponse.response.ResponseBase.Data,
88 | denomination ? denomination : 'ugnot'
89 | );
90 | }
91 |
92 | async getBlock(height: number): Promise {
93 | return await RestService.post(this.baseURL, {
94 | request: newRequest(BlockEndpoint.BLOCK, [height.toString()]),
95 | });
96 | }
97 |
98 | async getBlockResult(height: number): Promise {
99 | return await RestService.post(this.baseURL, {
100 | request: newRequest(BlockEndpoint.BLOCK_RESULTS, [height.toString()]),
101 | });
102 | }
103 |
104 | async getBlockNumber(): Promise {
105 | // Fetch the status for the latest info
106 | const status = await this.getStatus();
107 |
108 | return parseInt(status.sync_info.latest_block_height);
109 | }
110 |
111 | async getConsensusParams(height: number): Promise {
112 | return await RestService.post(this.baseURL, {
113 | request: newRequest(ConsensusEndpoint.CONSENSUS_PARAMS, [
114 | height.toString(),
115 | ]),
116 | });
117 | }
118 |
119 | getGasPrice(): Promise {
120 | return Promise.reject('not supported');
121 | }
122 |
123 | async getNetwork(): Promise {
124 | return await RestService.post(this.baseURL, {
125 | request: newRequest(ConsensusEndpoint.NET_INFO),
126 | });
127 | }
128 |
129 | async getAccountSequence(address: string, height?: number): Promise {
130 | const abciResponse: ABCIResponse = await RestService.post(
131 | this.baseURL,
132 | {
133 | request: newRequest(ABCIEndpoint.ABCI_QUERY, [
134 | `auth/accounts/${address}`,
135 | '',
136 | '0', // Height; not supported > 0 for now
137 | false,
138 | ]),
139 | }
140 | );
141 |
142 | return extractSequenceFromResponse(abciResponse.response.ResponseBase.Data);
143 | }
144 |
145 | async getAccountNumber(address: string, height?: number): Promise {
146 | const abciResponse: ABCIResponse = await RestService.post(
147 | this.baseURL,
148 | {
149 | request: newRequest(ABCIEndpoint.ABCI_QUERY, [
150 | `auth/accounts/${address}`,
151 | '',
152 | '0', // Height; not supported > 0 for now
153 | false,
154 | ]),
155 | }
156 | );
157 |
158 | return extractAccountNumberFromResponse(
159 | abciResponse.response.ResponseBase.Data
160 | );
161 | }
162 |
163 | async getStatus(): Promise {
164 | return await RestService.post(this.baseURL, {
165 | request: newRequest(CommonEndpoint.STATUS),
166 | });
167 | }
168 |
169 | async getTransaction(hash: string): Promise {
170 | return await RestService.post(this.baseURL, {
171 | request: newRequest(TransactionEndpoint.TX, [hash]),
172 | });
173 | }
174 |
175 | async sendTransaction(
176 | tx: string,
177 | endpoint: K
178 | ): Promise {
179 | const request: RPCRequest = newRequest(endpoint, [tx]);
180 |
181 | switch (endpoint) {
182 | case TransactionEndpoint.BROADCAST_TX_COMMIT:
183 | // The endpoint is a commit broadcast
184 | // (it waits for the transaction to be committed) to the chain before returning
185 | return this.broadcastTxCommit(request);
186 | case TransactionEndpoint.BROADCAST_TX_SYNC:
187 | default:
188 | return this.broadcastTxSync(request);
189 | }
190 | }
191 |
192 | private async broadcastTxSync(
193 | request: RPCRequest
194 | ): Promise {
195 | const response: BroadcastTxSyncResult =
196 | await RestService.post(this.baseURL, {
197 | request,
198 | });
199 |
200 | // Check if there is an immediate tx-broadcast error
201 | // (originating from basic transaction checks like CheckTx)
202 | if (response.error) {
203 | const errType: string = response.error[ABCIErrorKey];
204 | const log: string = response.Log;
205 |
206 | throw constructRequestError(errType, log);
207 | }
208 |
209 | return response;
210 | }
211 |
212 | private async broadcastTxCommit(
213 | request: RPCRequest
214 | ): Promise {
215 | const response: BroadcastTxCommitResult =
216 | await RestService.post(this.baseURL, {
217 | request,
218 | });
219 |
220 | const { check_tx, deliver_tx } = response;
221 |
222 | // Check if there is an immediate tx-broadcast error (in CheckTx)
223 | if (check_tx.ResponseBase.Error) {
224 | const errType: string = check_tx.ResponseBase.Error[ABCIErrorKey];
225 | const log: string = check_tx.ResponseBase.Log;
226 |
227 | throw constructRequestError(errType, log);
228 | }
229 |
230 | // Check if there is a parsing error with the transaction (in DeliverTx)
231 | if (deliver_tx.ResponseBase.Error) {
232 | const errType: string = deliver_tx.ResponseBase.Error[ABCIErrorKey];
233 | const log: string = deliver_tx.ResponseBase.Log;
234 |
235 | throw constructRequestError(errType, log);
236 | }
237 |
238 | return response;
239 | }
240 |
241 | async waitForTransaction(
242 | hash: string,
243 | fromHeight?: number,
244 | timeout?: number
245 | ): Promise {
246 | return waitForTransaction(this, hash, fromHeight, timeout);
247 | }
248 | }
249 |
--------------------------------------------------------------------------------
/src/provider/provider.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BlockInfo,
3 | BlockResult,
4 | BroadcastAsGeneric,
5 | BroadcastTransactionMap,
6 | ConsensusParams,
7 | NetworkInfo,
8 | Status,
9 | } from './types';
10 | import { Tx } from '../proto';
11 |
12 | /**
13 | * Read-only abstraction for accessing blockchain data
14 | */
15 | export interface Provider {
16 | // Account-specific methods //
17 |
18 | /**
19 | * Fetches the denomination balance of the account
20 | * @param {string} address the bech32 address of the account
21 | * @param {string} [denomination=ugnot] the balance denomination
22 | * @param {number} [height=0] the height for querying.
23 | * If omitted, the latest height is used
24 | */
25 | getBalance(
26 | address: string,
27 | denomination?: string,
28 | height?: number
29 | ): Promise;
30 |
31 | /**
32 | * Fetches the account sequence
33 | * @param {string} address the bech32 address of the account
34 | * @param {number} [height=0] the height for querying.
35 | * If omitted, the latest height is used.
36 | */
37 | getAccountSequence(address: string, height?: number): Promise;
38 |
39 | /**
40 | * Fetches the account number. Errors out if the account
41 | * is not initialized
42 | * @param {string} address the bech32 address of the account
43 | * @param {number} [height=0] the height for querying.
44 | * If omitted, the latest height is used
45 | */
46 | getAccountNumber(address: string, height?: number): Promise;
47 |
48 | /**
49 | * Fetches the block at the specific height, if any
50 | * @param {number} height the height for querying
51 | */
52 | getBlock(height: number): Promise;
53 |
54 | /**
55 | * Fetches the block at the specific height, if any
56 | * @param {number} height the height for querying
57 | */
58 | getBlockResult(height: number): Promise;
59 |
60 | /**
61 | * Fetches the latest block number from the chain
62 | */
63 | getBlockNumber(): Promise;
64 |
65 | // Network-specific methods //
66 |
67 | /**
68 | * Fetches the network information
69 | */
70 | getNetwork(): Promise;
71 |
72 | /**
73 | * Fetches the consensus params for the specific block height
74 | * @param {number} height the height for querying
75 | */
76 | getConsensusParams(height: number): Promise;
77 |
78 | /**
79 | * Fetches the current node status
80 | */
81 | getStatus(): Promise;
82 |
83 | /**
84 | * Fetches the current (recommended) average gas price
85 | */
86 | getGasPrice(): Promise;
87 |
88 | /**
89 | * Estimates the gas limit for the transaction
90 | * @param {Tx} tx the transaction that needs estimating
91 | */
92 | estimateGas(tx: Tx): Promise;
93 |
94 | // Transaction specific methods //
95 |
96 | /**
97 | * Sends the transaction to the node. If the type of endpoint
98 | * is a broadcast commit, waits for the transaction to be committed to the chain.
99 | * The transaction needs to be signed beforehand.
100 | * Returns the transaction broadcast result.
101 | * @param {string} tx the base64-encoded signed transaction
102 | * @param {BroadcastType} endpoint the transaction broadcast type (sync / commit)
103 | */
104 | sendTransaction(
105 | tx: string,
106 | endpoint: K
107 | ): Promise['result']>;
108 |
109 | /**
110 | * Waits for the transaction to be committed on the chain.
111 | * NOTE: This method will not take in the fromHeight parameter once
112 | * proper transaction indexing is added - the implementation should
113 | * simply try to fetch the transaction first to see if it's included in a block
114 | * before starting to wait for it; Until then, this method should be used
115 | * in the sequence:
116 | * get latest block -> send transaction -> waitForTransaction(block before send)
117 | * @param {string} hash The transaction hash
118 | * @param {number} [fromHeight=latest] The block height used to begin the search
119 | * @param {number} [timeout=15000] Optional wait timeout in MS
120 | */
121 | waitForTransaction(
122 | hash: string,
123 | fromHeight?: number,
124 | timeout?: number
125 | ): Promise;
126 | }
127 |
--------------------------------------------------------------------------------
/src/provider/types/abci.ts:
--------------------------------------------------------------------------------
1 | export interface ABCIResponse {
2 | response: {
3 | ResponseBase: ABCIResponseBase;
4 | Key: string | null;
5 | Value: string | null;
6 | Proof: MerkleProof | null;
7 | Height: string;
8 | };
9 | }
10 |
11 | export interface ABCIResponseBase {
12 | Error: {
13 | // ABCIErrorKey
14 | [key: string]: string;
15 | } | null;
16 | Data: string | null;
17 | Events: string | null;
18 | Log: string;
19 | Info: string;
20 | }
21 |
22 | interface MerkleProof {
23 | ops: {
24 | type: string;
25 | key: string | null;
26 | data: string | null;
27 | }[];
28 | }
29 |
30 | export interface ABCIAccount {
31 | BaseAccount: {
32 | // the associated account address
33 | address: string;
34 | // the balance list
35 | coins: string;
36 | // the public key info
37 | public_key: {
38 | // type of public key
39 | '@type': string;
40 | // public key value
41 | value: string;
42 | } | null;
43 | // the account number (state-dependent) (decimal)
44 | account_number: string;
45 | // the account sequence / nonce (decimal)
46 | sequence: string;
47 | };
48 | }
49 |
50 | export const ABCIErrorKey = '@type';
51 |
--------------------------------------------------------------------------------
/src/provider/types/common.ts:
--------------------------------------------------------------------------------
1 | import { ABCIResponseBase } from './abci';
2 | import { TransactionEndpoint } from '../endpoints';
3 |
4 | export interface NetworkInfo {
5 | // flag indicating if networking is up
6 | listening: boolean;
7 | // IDs of the listening peers
8 | listeners: string[];
9 | // the number of peers (decimal)
10 | n_peers: string;
11 | // the IDs of connected peers
12 | peers: string[];
13 | }
14 |
15 | export interface Status {
16 | // basic node information
17 | node_info: NodeInfo;
18 | // basic sync information
19 | sync_info: SyncInfo;
20 | // basic validator information
21 | validator_info: ValidatorInfo;
22 | }
23 |
24 | interface NodeInfo {
25 | // the version set of the node modules
26 | version_set: VersionInfo[];
27 | // validator address @ RPC endpoint
28 | net_address: string;
29 | // the chain ID
30 | network: string;
31 | software: string;
32 | // version of the Tendermint node
33 | version: string;
34 | channels: string;
35 | // user machine name
36 | monkier: string;
37 | other: {
38 | // type of enabled tx indexing ("off" when disabled)
39 | tx_index: string;
40 | // the TCP address of the node
41 | rpc_address: string;
42 | };
43 | }
44 |
45 | interface VersionInfo {
46 | // the name of the module
47 | Name: string;
48 | // the version of the module
49 | Version: string;
50 | // flag indicating if the module is optional
51 | Optional: boolean;
52 | }
53 |
54 | interface SyncInfo {
55 | // latest block hash
56 | latest_block_hash: string;
57 | // latest application hash
58 | latest_app_hash: string;
59 | // latest block height (decimal)
60 | latest_block_height: string;
61 | // latest block time in string format (ISO format)
62 | latest_block_time: string;
63 | // flag indicating if the node is syncing
64 | catching_up: boolean;
65 | }
66 |
67 | interface ValidatorInfo {
68 | // the address of the validator node
69 | address: string;
70 | // the validator's public key info
71 | pub_key: PublicKey;
72 | // the validator's voting power (decimal)
73 | voting_power: string;
74 | }
75 |
76 | interface PublicKey {
77 | // type of public key
78 | type: string;
79 | // public key value
80 | value: string;
81 | }
82 |
83 | export interface ConsensusParams {
84 | // the current block height
85 | block_height: string;
86 | // block consensus params
87 | consensus_params: {
88 | // the requested block
89 | Block: {
90 | // maximum tx size in bytes
91 | MaxTxBytes: string;
92 | // maximum data size in bytes
93 | MaxDataBytes: string;
94 | // maximum block size in bytes
95 | MaxBlockBytes: string;
96 | // block gas limit
97 | MaxGas: string;
98 | // block time in MS
99 | TimeIotaMS: string;
100 | };
101 | // validator info
102 | Validator: {
103 | // public key information
104 | PubKeyTypeURLs: string[];
105 | };
106 | };
107 | }
108 |
109 | export interface ConsensusState {
110 | // the current round state
111 | round_state: {
112 | // Required because of '/' in response fields (height/round/step)
113 | [key: string]: string | null | object;
114 | // the start time of the block
115 | start_time: string;
116 | // hash of the proposed block
117 | proposal_block_hash: string | null;
118 | // hash of the locked block
119 | locked_block_hash: string | null;
120 | // hash of the valid block
121 | valid_block_hash: string | null;
122 | // the vote set for the current height
123 | height_vote_set: object;
124 | };
125 | }
126 |
127 | export interface BlockInfo {
128 | // block metadata information
129 | block_meta: BlockMeta;
130 | // combined block info
131 | block: Block;
132 | }
133 |
134 | export interface BlockMeta {
135 | // the block parts
136 | block_id: BlockID;
137 | // the block header
138 | header: BlockHeader;
139 | }
140 |
141 | export interface Block {
142 | // the block header
143 | header: BlockHeader;
144 | // data contained in the block (txs)
145 | data: {
146 | // base64 encoded transactions
147 | txs: string[] | null;
148 | };
149 | // commit information
150 | last_commit: {
151 | // the block parts
152 | block_id: BlockID;
153 | // validator precommit information
154 | precommits: PrecommitInfo[] | null;
155 | };
156 | }
157 |
158 | export interface BlockHeader {
159 | // version of the node
160 | version: string;
161 | // the chain ID
162 | chain_id: string;
163 | // current height (decimal)
164 | height: string;
165 | // block creation time in string format (ISO format)
166 | time: string;
167 | // number of transactions (decimal)
168 | num_txs: string;
169 | // total number of transactions in the block (decimal)
170 | total_txs: string;
171 | // the current app version
172 | app_version: string;
173 | // parent block parts
174 | last_block_id: BlockID;
175 | // parent block commit hash
176 | last_commit_hash: string | null;
177 | // data hash (txs)
178 | data_hash: string | null;
179 | // validator set hash
180 | validators_hash: string;
181 | // consensus info hash
182 | consensus_hash: string;
183 | // app info hash
184 | app_hash: string;
185 | // last results hash
186 | last_results_hash: string | null;
187 | // address of the proposer
188 | proposer_address: string;
189 | }
190 |
191 | export interface BlockID {
192 | // the hash of the ID (block)
193 | hash: string | null;
194 | // part information
195 | parts: {
196 | // total number of parts (decimal)
197 | total: string;
198 | // the hash of the part
199 | hash: string | null;
200 | };
201 | }
202 |
203 | export interface PrecommitInfo {
204 | // type of precommit
205 | type: number;
206 | // the block height for the precommit
207 | height: string;
208 | // the round for the precommit
209 | round: string;
210 | // the block ID info
211 | block_id: BlockID;
212 | // precommit creation time (ISO format)
213 | timestamp: string;
214 | // the address of the validator who signed
215 | validator_address: string;
216 | // the index of the signer (validator)
217 | validator_index: string;
218 | // the base64 encoded signature of the signer (validator)
219 | signature: string;
220 | }
221 |
222 | export interface BlockResult {
223 | // the block height
224 | height: string;
225 | // block result info
226 | results: {
227 | // transactions contained in the block
228 | deliver_tx: DeliverTx[] | null;
229 | // end-block info
230 | end_block: EndBlock;
231 | // begin-block info
232 | begin_block: BeginBlock;
233 | };
234 | }
235 |
236 | export interface TxResult {
237 | // the transaction hash
238 | hash: string;
239 | // tx index in the block
240 | index: number;
241 | // the block height
242 | height: string;
243 | // deliver tx response
244 | tx_result: DeliverTx;
245 | // base64 encoded transaction
246 | tx: string;
247 | }
248 |
249 | export interface DeliverTx {
250 | // the transaction ABCI response
251 | ResponseBase: ABCIResponseBase;
252 | // transaction gas limit (decimal)
253 | GasWanted: string;
254 | // transaction actual gas used (decimal)
255 | GasUsed: string;
256 | }
257 |
258 | export interface EndBlock {
259 | // the block ABCI response
260 | ResponseBase: ABCIResponseBase;
261 | // validator update info
262 | ValidatorUpdates: string | null;
263 | // consensus params
264 | ConsensusParams: string | null;
265 | // block events
266 | Events: string | null;
267 | }
268 |
269 | export interface BeginBlock {
270 | // the block ABCI response
271 | ResponseBase: ABCIResponseBase;
272 | }
273 |
274 | export interface BroadcastTxSyncResult {
275 | error: {
276 | // ABCIErrorKey
277 | [key: string]: string;
278 | } | null;
279 | data: string | null;
280 | Log: string;
281 |
282 | hash: string;
283 | }
284 |
285 | export interface BroadcastTxCommitResult {
286 | check_tx: DeliverTx;
287 | deliver_tx: DeliverTx;
288 | hash: string;
289 | height: string; // decimal number
290 | }
291 |
292 | export type BroadcastType =
293 | | TransactionEndpoint.BROADCAST_TX_SYNC
294 | | TransactionEndpoint.BROADCAST_TX_COMMIT;
295 |
296 | export type BroadcastTransactionSync = {
297 | endpoint: TransactionEndpoint.BROADCAST_TX_SYNC;
298 | result: BroadcastTxSyncResult;
299 | };
300 |
301 | export type BroadcastTransactionCommit = {
302 | endpoint: TransactionEndpoint.BROADCAST_TX_COMMIT;
303 | result: BroadcastTxCommitResult;
304 | };
305 |
306 | export type BroadcastTransactionMap = {
307 | [TransactionEndpoint.BROADCAST_TX_COMMIT]: BroadcastTransactionCommit;
308 | [TransactionEndpoint.BROADCAST_TX_SYNC]: BroadcastTransactionSync;
309 | };
310 |
311 | export type BroadcastAsGeneric<
312 | K extends keyof BroadcastTransactionMap = keyof BroadcastTransactionMap,
313 | > = {
314 | [P in K]: BroadcastTransactionMap[P];
315 | }[K];
316 |
--------------------------------------------------------------------------------
/src/provider/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from './abci';
2 | export * from './common';
3 | export * from './jsonrpc';
4 |
--------------------------------------------------------------------------------
/src/provider/types/jsonrpc.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * The base JSON-RPC 2.0 request
3 | */
4 | export interface RPCRequest {
5 | jsonrpc: string;
6 | id: string | number;
7 | method: string;
8 |
9 | params?: any[];
10 | }
11 |
12 | /**
13 | * The base JSON-RPC 2.0 response
14 | */
15 | export interface RPCResponse {
16 | jsonrpc: string;
17 | id: string | number;
18 |
19 | result?: Result;
20 | error?: RPCError;
21 | }
22 |
23 | /**
24 | * The base JSON-RPC 2.0 typed response error
25 | */
26 | export interface RPCError {
27 | code: number;
28 | message: string;
29 |
30 | data?: any;
31 | }
32 |
--------------------------------------------------------------------------------
/src/provider/utility/errors.utility.ts:
--------------------------------------------------------------------------------
1 | import {
2 | GasOverflowError,
3 | InsufficientCoinsError,
4 | InsufficientFeeError,
5 | InsufficientFundsError,
6 | InternalError,
7 | InvalidAddressError,
8 | InvalidCoinsError,
9 | InvalidGasWantedError,
10 | InvalidPubKeyError,
11 | InvalidSequenceError,
12 | MemoTooLargeError,
13 | NoSignaturesError,
14 | OutOfGasError,
15 | TM2Error,
16 | TooManySignaturesError,
17 | TxDecodeError,
18 | UnauthorizedError,
19 | UnknownAddressError,
20 | UnknownRequestError,
21 | } from '../errors';
22 |
23 | /**
24 | * Constructs the appropriate Tendermint2
25 | * error based on the error ID.
26 | * Error IDs retrieved from:
27 | * https://github.com/gnolang/gno/blob/64f0fd0fa44021a076e1453b1767fbc914ed3b66/tm2/pkg/std/package.go#L20C1-L38
28 | * @param {string} errorID the proto ID of the error
29 | * @param {string} [log] the log associated with the error, if any
30 | * @returns {TM2Error}
31 | */
32 | export const constructRequestError = (
33 | errorID: string,
34 | log?: string
35 | ): TM2Error => {
36 | switch (errorID) {
37 | case '/std.InternalError':
38 | return new InternalError(log);
39 | case '/std.TxDecodeError':
40 | return new TxDecodeError(log);
41 | case '/std.InvalidSequenceError':
42 | return new InvalidSequenceError(log);
43 | case '/std.UnauthorizedError':
44 | return new UnauthorizedError(log);
45 | case '/std.InsufficientFundsError':
46 | return new InsufficientFundsError(log);
47 | case '/std.UnknownRequestError':
48 | return new UnknownRequestError(log);
49 | case '/std.InvalidAddressError':
50 | return new InvalidAddressError(log);
51 | case '/std.UnknownAddressError':
52 | return new UnknownAddressError(log);
53 | case '/std.InvalidPubKeyError':
54 | return new InvalidPubKeyError(log);
55 | case '/std.InsufficientCoinsError':
56 | return new InsufficientCoinsError(log);
57 | case '/std.InvalidCoinsError':
58 | return new InvalidCoinsError(log);
59 | case '/std.InvalidGasWantedError':
60 | return new InvalidGasWantedError(log);
61 | case '/std.OutOfGasError':
62 | return new OutOfGasError(log);
63 | case '/std.MemoTooLargeError':
64 | return new MemoTooLargeError(log);
65 | case '/std.InsufficientFeeError':
66 | return new InsufficientFeeError(log);
67 | case '/std.TooManySignaturesError':
68 | return new TooManySignaturesError(log);
69 | case '/std.NoSignaturesError':
70 | return new NoSignaturesError(log);
71 | case '/std.GasOverflowError':
72 | return new GasOverflowError(log);
73 | default:
74 | return new TM2Error(`unknown error: ${errorID}`, log);
75 | }
76 | };
77 |
--------------------------------------------------------------------------------
/src/provider/utility/index.ts:
--------------------------------------------------------------------------------
1 | export * from './provider.utility';
2 | export * from './requests.utility';
3 |
--------------------------------------------------------------------------------
/src/provider/utility/provider.utility.ts:
--------------------------------------------------------------------------------
1 | import { sha256 } from '@cosmjs/crypto';
2 | import { Tx } from '../../proto';
3 | import { ResponseDeliverTx } from '../../proto/tm2/abci';
4 | import { Provider } from '../provider';
5 | import { ABCIAccount, ABCIErrorKey, ABCIResponse, BlockInfo } from '../types';
6 | import { constructRequestError } from './errors.utility';
7 | import {
8 | base64ToUint8Array,
9 | parseABCI,
10 | parseProto,
11 | uint8ArrayToBase64,
12 | } from './requests.utility';
13 |
14 | /**
15 | * Extracts the specific balance denomination from the ABCI response
16 | * @param {string | null} abciData the base64-encoded ABCI data
17 | * @param {string} denomination the required denomination
18 | */
19 | export const extractBalanceFromResponse = (
20 | abciData: string | null,
21 | denomination: string
22 | ): number => {
23 | // Make sure the response is initialized
24 | if (!abciData) {
25 | return 0;
26 | }
27 |
28 | // Extract the balances
29 | const balancesRaw = Buffer.from(abciData, 'base64')
30 | .toString()
31 | .replace(/"/gi, '');
32 |
33 | // Find the correct balance denomination
34 | const balances: string[] = balancesRaw.split(',');
35 | if (balances.length < 1) {
36 | return 0;
37 | }
38 |
39 | // Find the correct denomination
40 | const pattern = new RegExp(`^(\\d+)${denomination}$`);
41 | for (const balance of balances) {
42 | const match = balance.match(pattern);
43 | if (match) {
44 | return parseInt(match[1], 10);
45 | }
46 | }
47 |
48 | return 0;
49 | };
50 |
51 | /**
52 | * Extracts the account sequence from the ABCI response
53 | * @param {string | null} abciData the base64-encoded ABCI data
54 | */
55 | export const extractSequenceFromResponse = (
56 | abciData: string | null
57 | ): number => {
58 | // Make sure the response is initialized
59 | if (!abciData) {
60 | return 0;
61 | }
62 |
63 | try {
64 | // Parse the account
65 | const account: ABCIAccount = parseABCI(abciData);
66 |
67 | return parseInt(account.BaseAccount.sequence, 10);
68 | } catch (e) {
69 | // unused case
70 | }
71 |
72 | // Account not initialized,
73 | // return default value (0)
74 | return 0;
75 | };
76 |
77 | /**
78 | * Extracts the account number from the ABCI response
79 | * @param {string | null} abciData the base64-encoded ABCI data
80 | */
81 | export const extractAccountNumberFromResponse = (
82 | abciData: string | null
83 | ): number => {
84 | // Make sure the response is initialized
85 | if (!abciData) {
86 | throw new Error('account is not initialized');
87 | }
88 |
89 | try {
90 | // Parse the account
91 | const account: ABCIAccount = parseABCI(abciData);
92 |
93 | return parseInt(account.BaseAccount.account_number, 10);
94 | } catch (e) {
95 | throw new Error('account is not initialized');
96 | }
97 | };
98 |
99 | /**
100 | * Extracts the simulate transaction response from the ABCI response value
101 | * @param {string | null} abciData the base64-encoded ResponseDeliverTx proto message
102 | */
103 | export const extractSimulateFromResponse = (
104 | abciResponse: ABCIResponse | null
105 | ): ResponseDeliverTx => {
106 | // Make sure the response is initialized
107 | if (!abciResponse) {
108 | throw new Error('abci data is not initialized');
109 | }
110 |
111 | const error = abciResponse.response?.ResponseBase?.Error;
112 | if (error && error[ABCIErrorKey]) {
113 | throw constructRequestError(error[ABCIErrorKey]);
114 | }
115 |
116 | const value = abciResponse.response.Value;
117 | if (!value) {
118 | throw new Error('abci data is not initialized');
119 | }
120 |
121 | try {
122 | return parseProto(value, ResponseDeliverTx.decode);
123 | } catch (e) {
124 | throw new Error('unable to parse simulate response');
125 | }
126 | };
127 |
128 | /**
129 | * Waits for the transaction to be committed to a block in the chain
130 | * of the specified provider. This helper does a search for incoming blocks
131 | * and checks if a transaction
132 | * @param {Provider} provider the provider instance
133 | * @param {string} hash the base64-encoded hash of the transaction
134 | * @param {number} [fromHeight=latest] the starting height for the search. If omitted, it is the latest block in the chain
135 | * @param {number} [timeout=15000] the timeout in MS for the search
136 | */
137 | export const waitForTransaction = async (
138 | provider: Provider,
139 | hash: string,
140 | fromHeight?: number,
141 | timeout?: number
142 | ): Promise => {
143 | return new Promise(async (resolve, reject) => {
144 | // Fetch the starting point
145 | let currentHeight = fromHeight
146 | ? fromHeight
147 | : await provider.getBlockNumber();
148 |
149 | const exitTimeout = timeout ? timeout : 15000;
150 |
151 | const fetchInterval = setInterval(async () => {
152 | // Fetch the latest block height
153 | const latestHeight = await provider.getBlockNumber();
154 |
155 | if (latestHeight < currentHeight) {
156 | // No need to parse older blocks
157 | return;
158 | }
159 |
160 | for (let blockNum = currentHeight; blockNum <= latestHeight; blockNum++) {
161 | // Fetch the block from the chain
162 | const block: BlockInfo = await provider.getBlock(blockNum);
163 |
164 | // Check if there are any transactions at all in the block
165 | if (!block.block.data.txs || block.block.data.txs.length == 0) {
166 | continue;
167 | }
168 |
169 | // Find the transaction among the block transactions
170 | for (const tx of block.block.data.txs) {
171 | // Decode the base-64 transaction
172 | const txRaw = base64ToUint8Array(tx);
173 |
174 | // Calculate the transaction hash
175 | const txHash = sha256(txRaw);
176 |
177 | if (uint8ArrayToBase64(txHash) == hash) {
178 | // Clear the interval
179 | clearInterval(fetchInterval);
180 |
181 | // Decode the transaction from amino
182 | resolve(Tx.decode(txRaw));
183 | }
184 | }
185 | }
186 |
187 | currentHeight = latestHeight + 1;
188 | }, 1000);
189 |
190 | setTimeout(() => {
191 | // Clear the fetch interval
192 | clearInterval(fetchInterval);
193 |
194 | reject('transaction fetch timeout');
195 | }, exitTimeout);
196 | });
197 | };
198 |
--------------------------------------------------------------------------------
/src/provider/utility/requests.utility.ts:
--------------------------------------------------------------------------------
1 | import { BinaryReader } from '@bufbuild/protobuf/wire';
2 | import { v4 as uuidv4 } from 'uuid';
3 | import { RPCError, RPCRequest, RPCResponse } from '../types';
4 |
5 | // The version of the supported JSON-RPC protocol
6 | const standardVersion = '2.0';
7 |
8 | /**
9 | * Creates a new JSON-RPC 2.0 request
10 | * @param {string} method the requested method
11 | * @param {string[]} [params] the requested params, if any
12 | */
13 | export const newRequest = (method: string, params?: any[]): RPCRequest => {
14 | return {
15 | // the ID of the request is not that relevant for this helper method;
16 | // for finer ID control, instantiate the request object directly
17 | id: uuidv4(),
18 | jsonrpc: standardVersion,
19 | method: method,
20 | params: params,
21 | };
22 | };
23 |
24 | /**
25 | * Creates a new JSON-RPC 2.0 response
26 | * @param {Result} result the response result, if any
27 | * @param {RPCError} error the response error, if any
28 | */
29 | export const newResponse = (
30 | result?: Result,
31 | error?: RPCError
32 | ): RPCResponse => {
33 | return {
34 | id: uuidv4(),
35 | jsonrpc: standardVersion,
36 | result: result,
37 | error: error,
38 | };
39 | };
40 |
41 | /**
42 | * Parses the base64 encoded ABCI JSON into a concrete type
43 | * @param {string} data the base64-encoded JSON
44 | */
45 | export const parseABCI = (data: string): Result => {
46 | const jsonData: string = Buffer.from(data, 'base64').toString();
47 | const parsedData: Result | null = JSON.parse(jsonData);
48 |
49 | if (!parsedData) {
50 | throw new Error('unable to parse JSON response');
51 | }
52 |
53 | return parsedData;
54 | };
55 |
56 | export const parseProto = (
57 | data: string,
58 | decodeFn: (input: BinaryReader | Uint8Array, length?: number) => T
59 | ) => {
60 | const protoData = decodeFn(Buffer.from(data, 'base64'));
61 |
62 | return protoData;
63 | };
64 |
65 | /**
66 | * Converts a string into base64 representation
67 | * @param {string} str the raw string
68 | */
69 | export const stringToBase64 = (str: string): string => {
70 | const buffer = Buffer.from(str, 'utf-8');
71 |
72 | return buffer.toString('base64');
73 | };
74 |
75 | /**
76 | * Converts a base64 string into a Uint8Array representation
77 | * @param {string} str the base64-encoded string
78 | */
79 | export const base64ToUint8Array = (str: string): Uint8Array => {
80 | const buffer = Buffer.from(str, 'base64');
81 |
82 | return new Uint8Array(buffer);
83 | };
84 |
85 | /**
86 | * Converts a Uint8Array into base64 representation
87 | * @param {Uint8Array} data the Uint8Array to be encoded
88 | */
89 | export const uint8ArrayToBase64 = (data: Uint8Array): string => {
90 | return Buffer.from(data).toString('base64');
91 | };
92 |
--------------------------------------------------------------------------------
/src/provider/websocket/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ws';
2 |
--------------------------------------------------------------------------------
/src/provider/websocket/ws.test.ts:
--------------------------------------------------------------------------------
1 | import { sha256 } from '@cosmjs/crypto';
2 | import WS from 'jest-websocket-mock';
3 | import Long from 'long';
4 | import { Tx } from '../../proto';
5 | import { CommonEndpoint, TransactionEndpoint } from '../endpoints';
6 | import { TM2Error } from '../errors';
7 | import { UnauthorizedErrorMessage } from '../errors/messages';
8 | import {
9 | ABCIAccount,
10 | ABCIErrorKey,
11 | ABCIResponse,
12 | ABCIResponseBase,
13 | BeginBlock,
14 | BlockInfo,
15 | BlockResult,
16 | BroadcastTxSyncResult,
17 | ConsensusParams,
18 | EndBlock,
19 | NetworkInfo,
20 | Status,
21 | } from '../types';
22 | import { newResponse, stringToBase64, uint8ArrayToBase64 } from '../utility';
23 | import { WSProvider } from './ws';
24 |
25 | describe('WS Provider', () => {
26 | const wsPort = 8545;
27 | const wsHost = 'localhost';
28 | const wsURL = `ws://${wsHost}:${wsPort}`;
29 |
30 | let server: WS;
31 | let wsProvider: WSProvider;
32 |
33 | const mockABCIResponse = (response: string): ABCIResponse => {
34 | return {
35 | response: {
36 | ResponseBase: {
37 | Log: '',
38 | Info: '',
39 | Data: stringToBase64(response),
40 | Error: null,
41 | Events: null,
42 | },
43 | Key: null,
44 | Value: null,
45 | Proof: null,
46 | Height: '',
47 | },
48 | };
49 | };
50 |
51 | /**
52 | * Sets up the test response handler (single-response)
53 | * @param {WebSocketServer} wss the websocket server returning data
54 | * @param {Type} testData the test data being returned to the client
55 | */
56 | const setHandler = async (testData: Type) => {
57 | server.on('connection', (socket) => {
58 | socket.on('message', (data) => {
59 | const request = JSON.parse(data.toString());
60 | const response = newResponse(testData);
61 | response.id = request.id;
62 |
63 | socket.send(JSON.stringify(response));
64 | });
65 | });
66 |
67 | await server.connected;
68 | };
69 |
70 | beforeEach(() => {
71 | server = new WS(wsURL);
72 | wsProvider = new WSProvider(wsURL);
73 | });
74 |
75 | afterEach(() => {
76 | wsProvider.closeConnection();
77 | WS.clean();
78 | });
79 |
80 | test('estimateGas', async () => {
81 | const tx = Tx.fromJSON({
82 | signatures: [],
83 | fee: {
84 | gasFee: '',
85 | gasWanted: new Long(0),
86 | },
87 | messages: [],
88 | memo: '',
89 | });
90 | const expectedEstimation = 44900;
91 |
92 | const mockSimulateResponseVale =
93 | 'CiMiIW1zZzowLHN1Y2Nlc3M6dHJ1ZSxsb2c6LGV2ZW50czpbXRCAiXoYyL0F';
94 |
95 | const mockABCIResponse: ABCIResponse = {
96 | response: {
97 | Height: '',
98 | Key: '',
99 | Proof: null,
100 | Value: mockSimulateResponseVale,
101 | ResponseBase: {
102 | Log: '',
103 | Info: '',
104 | Error: null,
105 | Events: null,
106 | Data: '',
107 | },
108 | },
109 | };
110 |
111 | // Set the response
112 | await setHandler(mockABCIResponse);
113 |
114 | const estimation = await wsProvider.estimateGas(tx);
115 |
116 | expect(estimation).toEqual(expectedEstimation);
117 | });
118 |
119 | test('getNetwork', async () => {
120 | const mockInfo: NetworkInfo = {
121 | listening: false,
122 | listeners: [],
123 | n_peers: '0',
124 | peers: [],
125 | };
126 |
127 | // Set the response
128 | await setHandler(mockInfo);
129 |
130 | const info: NetworkInfo = await wsProvider.getNetwork();
131 | expect(info).toEqual(mockInfo);
132 | });
133 |
134 | const getEmptyStatus = (): Status => {
135 | return {
136 | node_info: {
137 | version_set: [],
138 | net_address: '',
139 | network: '',
140 | software: '',
141 | version: '',
142 | channels: '',
143 | monkier: '',
144 | other: {
145 | tx_index: '',
146 | rpc_address: '',
147 | },
148 | },
149 | sync_info: {
150 | latest_block_hash: '',
151 | latest_app_hash: '',
152 | latest_block_height: '',
153 | latest_block_time: '',
154 | catching_up: false,
155 | },
156 | validator_info: {
157 | address: '',
158 | pub_key: {
159 | type: '',
160 | value: '',
161 | },
162 | voting_power: '',
163 | },
164 | };
165 | };
166 |
167 | test('getStatus', async () => {
168 | const mockStatus: Status = getEmptyStatus();
169 | mockStatus.validator_info.address = 'address';
170 |
171 | // Set the response
172 | await setHandler(mockStatus);
173 |
174 | const status: Status = await wsProvider.getStatus();
175 | expect(status).toEqual(status);
176 | });
177 |
178 | test('getConsensusParams', async () => {
179 | const mockParams: ConsensusParams = {
180 | block_height: '',
181 | consensus_params: {
182 | Block: {
183 | MaxTxBytes: '',
184 | MaxDataBytes: '',
185 | MaxBlockBytes: '',
186 | MaxGas: '',
187 | TimeIotaMS: '',
188 | },
189 | Validator: {
190 | PubKeyTypeURLs: [],
191 | },
192 | },
193 | };
194 |
195 | // Set the response
196 | await setHandler(mockParams);
197 |
198 | const params: ConsensusParams = await wsProvider.getConsensusParams(1);
199 | expect(params).toEqual(mockParams);
200 | });
201 |
202 | describe('getSequence', () => {
203 | const validAccount: ABCIAccount = {
204 | BaseAccount: {
205 | address: 'random address',
206 | coins: '',
207 | public_key: null,
208 | account_number: '0',
209 | sequence: '10',
210 | },
211 | };
212 |
213 | test.each([
214 | [
215 | JSON.stringify(validAccount),
216 | parseInt(validAccount.BaseAccount.sequence, 10),
217 | ], // account exists
218 | ['null', 0], // account doesn't exist
219 | ])('case %#', async (response, expected) => {
220 | const mockResponse: ABCIResponse = mockABCIResponse(response);
221 |
222 | // Set the response
223 | await setHandler(mockResponse);
224 |
225 | const sequence: number = await wsProvider.getAccountSequence('address');
226 | expect(sequence).toBe(expected);
227 | });
228 | });
229 |
230 | describe('getAccountNumber', () => {
231 | const validAccount: ABCIAccount = {
232 | BaseAccount: {
233 | address: 'random address',
234 | coins: '',
235 | public_key: null,
236 | account_number: '10',
237 | sequence: '0',
238 | },
239 | };
240 |
241 | test.each([
242 | [
243 | JSON.stringify(validAccount),
244 | parseInt(validAccount.BaseAccount.account_number, 10),
245 | ], // account exists
246 | ['null', 0], // account doesn't exist
247 | ])('case %#', async (response, expected) => {
248 | const mockResponse: ABCIResponse = mockABCIResponse(response);
249 |
250 | // Set the response
251 | await setHandler(mockResponse);
252 |
253 | try {
254 | const accountNumber: number =
255 | await wsProvider.getAccountNumber('address');
256 | expect(accountNumber).toBe(expected);
257 | } catch (e) {
258 | expect((e as Error).message).toContain('account is not initialized');
259 | }
260 | });
261 | });
262 |
263 | describe('getBalance', () => {
264 | const denomination = 'atom';
265 | test.each([
266 | ['"5gnot,100atom"', 100], // balance found
267 | ['"5universe"', 0], // balance not found
268 | ['""', 0], // account doesn't exist
269 | ])('case %#', async (existing, expected) => {
270 | const mockResponse: ABCIResponse = mockABCIResponse(existing);
271 |
272 | // Set the response
273 | await setHandler(mockResponse);
274 |
275 | const balance: number = await wsProvider.getBalance(
276 | 'address',
277 | denomination
278 | );
279 | expect(balance).toBe(expected);
280 | });
281 | });
282 |
283 | test('getBlockNumber', async () => {
284 | const expectedBlockNumber = 10;
285 | const mockStatus: Status = getEmptyStatus();
286 | mockStatus.sync_info.latest_block_height = `${expectedBlockNumber}`;
287 |
288 | // Set the response
289 | await setHandler(mockStatus);
290 |
291 | const blockNumber: number = await wsProvider.getBlockNumber();
292 | expect(blockNumber).toBe(expectedBlockNumber);
293 | });
294 |
295 | describe('sendTransaction', () => {
296 | const validResult: BroadcastTxSyncResult = {
297 | error: null,
298 | data: null,
299 | Log: '',
300 | hash: 'hash123',
301 | };
302 |
303 | const mockError = '/std.UnauthorizedError';
304 | const mockLog = 'random error message';
305 | const invalidResult: BroadcastTxSyncResult = {
306 | error: {
307 | [ABCIErrorKey]: mockError,
308 | },
309 | data: null,
310 | Log: mockLog,
311 | hash: '',
312 | };
313 |
314 | test.each([
315 | [validResult, validResult.hash, '', ''], // no error
316 | [invalidResult, invalidResult.hash, UnauthorizedErrorMessage, mockLog], // error out
317 | ])('case %#', async (response, expectedHash, expectedErr, expectedLog) => {
318 | await setHandler(response);
319 |
320 | try {
321 | const tx = await wsProvider.sendTransaction(
322 | 'encoded tx',
323 | TransactionEndpoint.BROADCAST_TX_SYNC
324 | );
325 |
326 | expect(tx.hash).toEqual(expectedHash);
327 |
328 | if (expectedErr != '') {
329 | fail('expected error');
330 | }
331 | } catch (e) {
332 | expect((e as Error).message).toBe(expectedErr);
333 | expect((e as TM2Error).log).toBe(expectedLog);
334 | }
335 | });
336 | });
337 |
338 | const getEmptyBlockInfo = (): BlockInfo => {
339 | const emptyHeader = {
340 | version: '',
341 | chain_id: '',
342 | height: '',
343 | time: '',
344 | num_txs: '',
345 | total_txs: '',
346 | app_version: '',
347 | last_block_id: {
348 | hash: null,
349 | parts: {
350 | total: '',
351 | hash: null,
352 | },
353 | },
354 | last_commit_hash: '',
355 | data_hash: '',
356 | validators_hash: '',
357 | consensus_hash: '',
358 | app_hash: '',
359 | last_results_hash: '',
360 | proposer_address: '',
361 | };
362 |
363 | const emptyBlockID = {
364 | hash: null,
365 | parts: {
366 | total: '',
367 | hash: null,
368 | },
369 | };
370 |
371 | return {
372 | block_meta: {
373 | block_id: emptyBlockID,
374 | header: emptyHeader,
375 | },
376 | block: {
377 | header: emptyHeader,
378 | data: {
379 | txs: null,
380 | },
381 | last_commit: {
382 | block_id: emptyBlockID,
383 | precommits: null,
384 | },
385 | },
386 | };
387 | };
388 |
389 | test('getBlock', async () => {
390 | const mockInfo: BlockInfo = getEmptyBlockInfo();
391 |
392 | // Set the response
393 | await setHandler(mockInfo);
394 |
395 | const result: BlockInfo = await wsProvider.getBlock(0);
396 | expect(result).toEqual(mockInfo);
397 | });
398 |
399 | const getEmptyBlockResult = (): BlockResult => {
400 | const emptyResponseBase: ABCIResponseBase = {
401 | Error: null,
402 | Data: null,
403 | Events: null,
404 | Log: '',
405 | Info: '',
406 | };
407 |
408 | const emptyEndBlock: EndBlock = {
409 | ResponseBase: emptyResponseBase,
410 | ValidatorUpdates: null,
411 | ConsensusParams: null,
412 | Events: null,
413 | };
414 |
415 | const emptyStartBlock: BeginBlock = {
416 | ResponseBase: emptyResponseBase,
417 | };
418 |
419 | return {
420 | height: '',
421 | results: {
422 | deliver_tx: null,
423 | end_block: emptyEndBlock,
424 | begin_block: emptyStartBlock,
425 | },
426 | };
427 | };
428 |
429 | test('getBlockResult', async () => {
430 | const mockResult: BlockResult = getEmptyBlockResult();
431 |
432 | // Set the response
433 | await setHandler(mockResult);
434 |
435 | const result: BlockResult = await wsProvider.getBlockResult(0);
436 | expect(result).toEqual(mockResult);
437 | });
438 |
439 | test('waitForTransaction', async () => {
440 | const emptyBlock: BlockInfo = getEmptyBlockInfo();
441 | emptyBlock.block.data = {
442 | txs: [],
443 | };
444 |
445 | const tx: Tx = {
446 | messages: [],
447 | signatures: [],
448 | memo: 'tx memo',
449 | };
450 |
451 | const encodedTx = Tx.encode(tx).finish();
452 | const txHash = sha256(encodedTx);
453 |
454 | const filledBlock: BlockInfo = getEmptyBlockInfo();
455 | filledBlock.block.data = {
456 | txs: [uint8ArrayToBase64(encodedTx)],
457 | };
458 |
459 | const latestBlock = 5;
460 | const startBlock = latestBlock - 2;
461 |
462 | const mockStatus: Status = getEmptyStatus();
463 | mockStatus.sync_info.latest_block_height = `${latestBlock}`;
464 |
465 | const responseMap: Map = new Map([
466 | [latestBlock, filledBlock],
467 | [latestBlock - 1, emptyBlock],
468 | [startBlock, emptyBlock],
469 | ]);
470 |
471 | // Set the response
472 | server.on('connection', (socket) => {
473 | socket.on('message', (data) => {
474 | const request = JSON.parse(data.toString());
475 |
476 | if (request.method == CommonEndpoint.STATUS) {
477 | const response = newResponse(mockStatus);
478 | response.id = request.id;
479 |
480 | socket.send(JSON.stringify(response));
481 |
482 | return;
483 | }
484 |
485 | if (!request.params) {
486 | return;
487 | }
488 |
489 | const blockNum: number = +(request.params[0] as string[]);
490 | const info = responseMap.get(blockNum);
491 |
492 | const response = newResponse(info);
493 | response.id = request.id;
494 |
495 | socket.send(JSON.stringify(response));
496 | });
497 | });
498 |
499 | await server.connected;
500 |
501 | const receivedTx: Tx = await wsProvider.waitForTransaction(
502 | uint8ArrayToBase64(txHash),
503 | startBlock
504 | );
505 | expect(receivedTx).toEqual(tx);
506 | });
507 | });
508 |
--------------------------------------------------------------------------------
/src/provider/websocket/ws.ts:
--------------------------------------------------------------------------------
1 | import { Tx } from '../../proto';
2 | import {
3 | ABCIEndpoint,
4 | BlockEndpoint,
5 | CommonEndpoint,
6 | ConsensusEndpoint,
7 | TransactionEndpoint,
8 | } from '../endpoints';
9 | import { Provider } from '../provider';
10 | import {
11 | ABCIErrorKey,
12 | ABCIResponse,
13 | BlockInfo,
14 | BlockResult,
15 | BroadcastTransactionMap,
16 | BroadcastTxCommitResult,
17 | BroadcastTxSyncResult,
18 | ConsensusParams,
19 | NetworkInfo,
20 | RPCRequest,
21 | RPCResponse,
22 | Status,
23 | TxResult,
24 | } from '../types';
25 | import {
26 | extractAccountNumberFromResponse,
27 | extractBalanceFromResponse,
28 | extractSequenceFromResponse,
29 | extractSimulateFromResponse,
30 | newRequest,
31 | uint8ArrayToBase64,
32 | waitForTransaction,
33 | } from '../utility';
34 | import { constructRequestError } from '../utility/errors.utility';
35 |
36 | /**
37 | * Provider based on WS JSON-RPC HTTP requests
38 | */
39 | export class WSProvider implements Provider {
40 | protected ws: WebSocket; // the persistent WS connection
41 | protected readonly requestMap: Map<
42 | number | string,
43 | {
44 | resolve: (response: RPCResponse) => void;
45 | reject: (reason: Error) => void;
46 | timeout: NodeJS.Timeout;
47 | }
48 | > = new Map(); // callback method map for the individual endpoints
49 | protected requestTimeout = 15000; // 15s
50 |
51 | /**
52 | * Creates a new instance of the {@link WSProvider}
53 | * @param {string} baseURL the WS URL of the node
54 | * @param {number} requestTimeout the timeout for the WS request (in MS)
55 | */
56 | constructor(baseURL: string, requestTimeout?: number) {
57 | this.ws = new WebSocket(baseURL);
58 |
59 | this.ws.addEventListener('message', (event) => {
60 | const response = JSON.parse(event.data as string) as RPCResponse;
61 | const request = this.requestMap.get(response.id);
62 | if (request) {
63 | this.requestMap.delete(response.id);
64 | clearTimeout(request.timeout);
65 |
66 | request.resolve(response);
67 | }
68 |
69 | // Set the default timeout
70 | this.requestTimeout = requestTimeout ? requestTimeout : 15000;
71 | });
72 | }
73 |
74 | /**
75 | * Closes the WS connection. Required when done working
76 | * with the WS provider
77 | */
78 | closeConnection() {
79 | this.ws.close();
80 | }
81 |
82 | /**
83 | * Sends a request to the WS connection, and resolves
84 | * upon receiving the response
85 | * @param {RPCRequest} request the RPC request
86 | */
87 | async sendRequest(request: RPCRequest): Promise> {
88 | // Make sure the connection is open
89 | if (this.ws.readyState != WebSocket.OPEN) {
90 | await this.waitForOpenConnection();
91 | }
92 |
93 | // The promise will resolve as soon as the response is received
94 | const promise = new Promise>((resolve, reject) => {
95 | const timeout = setTimeout(() => {
96 | this.requestMap.delete(request.id);
97 |
98 | reject(new Error('Request timed out'));
99 | }, this.requestTimeout);
100 |
101 | this.requestMap.set(request.id, { resolve, reject, timeout });
102 | });
103 |
104 | this.ws.send(JSON.stringify(request));
105 |
106 | return promise;
107 | }
108 |
109 | /**
110 | * Parses the result from the response
111 | * @param {RPCResponse} response the response to be parsed
112 | */
113 | parseResponse(response: RPCResponse): Result {
114 | if (!response) {
115 | throw new Error('invalid response');
116 | }
117 |
118 | if (response.error) {
119 | throw new Error(response.error?.message);
120 | }
121 |
122 | if (!response.result) {
123 | throw new Error('invalid response returned');
124 | }
125 |
126 | return response.result;
127 | }
128 |
129 | /**
130 | * Waits for the WS connection to be established
131 | */
132 | waitForOpenConnection = (): Promise => {
133 | return new Promise((resolve, reject) => {
134 | const maxNumberOfAttempts = 20;
135 | const intervalTime = 500; // ms
136 |
137 | let currentAttempt = 0;
138 | const interval = setInterval(() => {
139 | if (this.ws.readyState === WebSocket.OPEN) {
140 | clearInterval(interval);
141 | resolve(null);
142 | }
143 |
144 | currentAttempt++;
145 | if (currentAttempt >= maxNumberOfAttempts) {
146 | clearInterval(interval);
147 | reject(new Error('Unable to establish WS connection'));
148 | }
149 | }, intervalTime);
150 | });
151 | };
152 |
153 | async estimateGas(tx: Tx): Promise {
154 | const encodedTx = uint8ArrayToBase64(Tx.encode(tx).finish());
155 | const response = await this.sendRequest(
156 | newRequest(ABCIEndpoint.ABCI_QUERY, [
157 | `.app/simulate`,
158 | `${encodedTx}`,
159 | '0', // Height; not supported > 0 for now
160 | false,
161 | ])
162 | );
163 |
164 | // Parse the response
165 | const abciResponse = this.parseResponse(response);
166 |
167 | const simulateResult = extractSimulateFromResponse(abciResponse);
168 |
169 | const resultErrorKey = simulateResult.responseBase?.error?.typeUrl;
170 | if (resultErrorKey) {
171 | throw constructRequestError(resultErrorKey);
172 | }
173 |
174 | return simulateResult.gasUsed.toInt();
175 | }
176 |
177 | async getBalance(
178 | address: string,
179 | denomination?: string,
180 | height?: number
181 | ): Promise {
182 | const response = await this.sendRequest(
183 | newRequest(ABCIEndpoint.ABCI_QUERY, [
184 | `bank/balances/${address}`,
185 | '',
186 | '0', // Height; not supported > 0 for now
187 | false,
188 | ])
189 | );
190 |
191 | // Parse the response
192 | const abciResponse = this.parseResponse(response);
193 |
194 | return extractBalanceFromResponse(
195 | abciResponse.response.ResponseBase.Data,
196 | denomination ? denomination : 'ugnot'
197 | );
198 | }
199 |
200 | async getBlock(height: number): Promise {
201 | const response = await this.sendRequest(
202 | newRequest(BlockEndpoint.BLOCK, [height.toString()])
203 | );
204 |
205 | return this.parseResponse(response);
206 | }
207 |
208 | async getBlockResult(height: number): Promise {
209 | const response = await this.sendRequest(
210 | newRequest(BlockEndpoint.BLOCK_RESULTS, [height.toString()])
211 | );
212 |
213 | return this.parseResponse(response);
214 | }
215 |
216 | async getBlockNumber(): Promise {
217 | // Fetch the status for the latest info
218 | const status = await this.getStatus();
219 |
220 | return parseInt(status.sync_info.latest_block_height);
221 | }
222 |
223 | async getConsensusParams(height: number): Promise {
224 | const response = await this.sendRequest(
225 | newRequest(ConsensusEndpoint.CONSENSUS_PARAMS, [height.toString()])
226 | );
227 |
228 | return this.parseResponse(response);
229 | }
230 |
231 | getGasPrice(): Promise {
232 | return Promise.reject('implement me');
233 | }
234 |
235 | async getNetwork(): Promise {
236 | const response = await this.sendRequest(
237 | newRequest(ConsensusEndpoint.NET_INFO)
238 | );
239 |
240 | return this.parseResponse(response);
241 | }
242 |
243 | async getAccountSequence(address: string, height?: number): Promise {
244 | const response = await this.sendRequest(
245 | newRequest(ABCIEndpoint.ABCI_QUERY, [
246 | `auth/accounts/${address}`,
247 | '',
248 | '0', // Height; not supported > 0 for now
249 | false,
250 | ])
251 | );
252 |
253 | // Parse the response
254 | const abciResponse = this.parseResponse(response);
255 |
256 | return extractSequenceFromResponse(abciResponse.response.ResponseBase.Data);
257 | }
258 |
259 | async getAccountNumber(address: string, height?: number): Promise {
260 | const response = await this.sendRequest(
261 | newRequest(ABCIEndpoint.ABCI_QUERY, [
262 | `auth/accounts/${address}`,
263 | '',
264 | '0', // Height; not supported > 0 for now
265 | false,
266 | ])
267 | );
268 |
269 | // Parse the response
270 | const abciResponse = this.parseResponse(response);
271 |
272 | return extractAccountNumberFromResponse(
273 | abciResponse.response.ResponseBase.Data
274 | );
275 | }
276 |
277 | async getStatus(): Promise {
278 | const response = await this.sendRequest(
279 | newRequest(CommonEndpoint.STATUS)
280 | );
281 |
282 | return this.parseResponse(response);
283 | }
284 |
285 | async getTransaction(hash: string): Promise {
286 | const response = await this.sendRequest(
287 | newRequest(TransactionEndpoint.TX, [hash])
288 | );
289 |
290 | return this.parseResponse(response);
291 | }
292 |
293 | async sendTransaction(
294 | tx: string,
295 | endpoint: K
296 | ): Promise {
297 | const request: RPCRequest = newRequest(endpoint, [tx]);
298 |
299 | switch (endpoint) {
300 | case TransactionEndpoint.BROADCAST_TX_COMMIT:
301 | // The endpoint is a commit broadcast
302 | // (it waits for the transaction to be committed) to the chain before returning
303 | return this.broadcastTxCommit(request);
304 | case TransactionEndpoint.BROADCAST_TX_SYNC:
305 | default:
306 | return this.broadcastTxSync(request);
307 | }
308 | }
309 |
310 | private async broadcastTxSync(
311 | request: RPCRequest
312 | ): Promise {
313 | const response: RPCResponse =
314 | await this.sendRequest(request);
315 |
316 | const broadcastResponse: BroadcastTxSyncResult =
317 | this.parseResponse(response);
318 |
319 | // Check if there is an immediate tx-broadcast error
320 | // (originating from basic transaction checks like CheckTx)
321 | if (broadcastResponse.error) {
322 | const errType: string = broadcastResponse.error[ABCIErrorKey];
323 | const log: string = broadcastResponse.Log;
324 |
325 | throw constructRequestError(errType, log);
326 | }
327 |
328 | return broadcastResponse;
329 | }
330 |
331 | private async broadcastTxCommit(
332 | request: RPCRequest
333 | ): Promise {
334 | const response: RPCResponse =
335 | await this.sendRequest(request);
336 |
337 | const broadcastResponse: BroadcastTxCommitResult =
338 | this.parseResponse(response);
339 |
340 | const { check_tx, deliver_tx } = broadcastResponse;
341 |
342 | // Check if there is an immediate tx-broadcast error (in CheckTx)
343 | if (check_tx.ResponseBase.Error) {
344 | const errType: string = check_tx.ResponseBase.Error[ABCIErrorKey];
345 | const log: string = check_tx.ResponseBase.Log;
346 |
347 | throw constructRequestError(errType, log);
348 | }
349 |
350 | // Check if there is a parsing error with the transaction (in DeliverTx)
351 | if (deliver_tx.ResponseBase.Error) {
352 | const errType: string = deliver_tx.ResponseBase.Error[ABCIErrorKey];
353 | const log: string = deliver_tx.ResponseBase.Log;
354 |
355 | throw constructRequestError(errType, log);
356 | }
357 |
358 | return broadcastResponse;
359 | }
360 |
361 | waitForTransaction(
362 | hash: string,
363 | fromHeight?: number,
364 | timeout?: number
365 | ): Promise {
366 | return waitForTransaction(this, hash, fromHeight, timeout);
367 | }
368 | }
369 |
--------------------------------------------------------------------------------
/src/services/index.ts:
--------------------------------------------------------------------------------
1 | export * from './rest/restService';
2 | export * from './rest/restService.types';
3 |
--------------------------------------------------------------------------------
/src/services/rest/restService.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { RequestParams } from './restService.types';
3 | import { RPCResponse } from '../../provider';
4 |
5 | export class RestService {
6 | static async post(
7 | baseURL: string,
8 | params: RequestParams
9 | ): Promise {
10 | const axiosResponse = await axios.post>(
11 | baseURL,
12 | params.request,
13 | params.config
14 | );
15 |
16 | const { result, error } = axiosResponse.data;
17 |
18 | // Check for errors
19 | if (error) {
20 | // Error encountered during the POST request
21 | throw new Error(`${error.message}: ${error.data}`);
22 | }
23 |
24 | // Check for the correct result format
25 | if (!result) {
26 | throw new Error('invalid result returned');
27 | }
28 |
29 | return result;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/services/rest/restService.types.ts:
--------------------------------------------------------------------------------
1 | import { AxiosRequestConfig } from 'axios';
2 | import { RPCRequest } from '../../provider';
3 |
4 | export interface RequestParams {
5 | request: RPCRequest;
6 | config?: AxiosRequestConfig;
7 | }
8 |
--------------------------------------------------------------------------------
/src/wallet/index.ts:
--------------------------------------------------------------------------------
1 | export * from './key';
2 | export * from './ledger';
3 | export * from './signer';
4 | export * from './types';
5 | export * from './utility';
6 | export * from './wallet';
7 |
--------------------------------------------------------------------------------
/src/wallet/key/index.ts:
--------------------------------------------------------------------------------
1 | export * from './key';
2 |
--------------------------------------------------------------------------------
/src/wallet/key/key.test.ts:
--------------------------------------------------------------------------------
1 | import { generateEntropy, generateKeyPair, stringToUTF8 } from '../utility';
2 | import { entropyToMnemonic } from '@cosmjs/crypto/build/bip39';
3 | import { KeySigner } from './key';
4 |
5 | describe('Private Key Signer', () => {
6 | const generateRandomKeySigner = async (
7 | index?: number
8 | ): Promise => {
9 | const { publicKey, privateKey } = await generateKeyPair(
10 | entropyToMnemonic(generateEntropy()),
11 | index ? index : 0
12 | );
13 |
14 | return new KeySigner(privateKey, publicKey);
15 | };
16 |
17 | test('getAddress', async () => {
18 | const signer: KeySigner = await generateRandomKeySigner();
19 | const address: string = await signer.getAddress();
20 |
21 | expect(address.length).toBe(40);
22 | });
23 |
24 | test('getPublicKey', async () => {
25 | const signer: KeySigner = await generateRandomKeySigner();
26 | const publicKey: Uint8Array = await signer.getPublicKey();
27 |
28 | expect(publicKey).not.toBeNull();
29 | expect(publicKey).toHaveLength(65);
30 | });
31 |
32 | test('getPrivateKey', async () => {
33 | const signer: KeySigner = await generateRandomKeySigner();
34 | const privateKey: Uint8Array = await signer.getPrivateKey();
35 |
36 | expect(privateKey).not.toBeNull();
37 | expect(privateKey).toHaveLength(32);
38 | });
39 |
40 | test('signData', async () => {
41 | const rawData: Uint8Array = stringToUTF8('random raw data');
42 | const signer: KeySigner = await generateRandomKeySigner();
43 |
44 | // Sign the data
45 | const signature: Uint8Array = await signer.signData(rawData);
46 |
47 | // Verify the signature
48 | const isValid: boolean = await signer.verifySignature(rawData, signature);
49 |
50 | expect(isValid).toBe(true);
51 | });
52 | });
53 |
--------------------------------------------------------------------------------
/src/wallet/key/key.ts:
--------------------------------------------------------------------------------
1 | import { Signer } from '../signer';
2 | import { encodeSecp256k1Pubkey, pubkeyToAddress } from '@cosmjs/amino';
3 | import { Secp256k1, Secp256k1Signature, sha256 } from '@cosmjs/crypto';
4 | import { defaultAddressPrefix } from '../utility';
5 |
6 | /**
7 | * KeySigner implements the logic for the private key signer
8 | */
9 | export class KeySigner implements Signer {
10 | private readonly privateKey: Uint8Array; // the raw private key
11 | private readonly publicKey: Uint8Array; // the compressed public key
12 | private readonly addressPrefix: string; // the address prefix
13 |
14 | /**
15 | * Creates a new {@link KeySigner} instance
16 | * @param {Uint8Array} privateKey the raw Secp256k1 private key
17 | * @param {Uint8Array} publicKey the raw Secp256k1 public key
18 | * @param {string} addressPrefix the address prefix
19 | */
20 | constructor(
21 | privateKey: Uint8Array,
22 | publicKey: Uint8Array,
23 | addressPrefix: string = defaultAddressPrefix
24 | ) {
25 | this.privateKey = privateKey;
26 | this.publicKey = publicKey;
27 | this.addressPrefix = addressPrefix;
28 | }
29 |
30 | getAddress = async (): Promise => {
31 | return pubkeyToAddress(
32 | encodeSecp256k1Pubkey(Secp256k1.compressPubkey(this.publicKey)),
33 | this.addressPrefix
34 | );
35 | };
36 |
37 | getPublicKey = async (): Promise => {
38 | return this.publicKey;
39 | };
40 |
41 | getPrivateKey = async (): Promise => {
42 | return this.privateKey;
43 | };
44 |
45 | signData = async (data: Uint8Array): Promise => {
46 | const signature = await Secp256k1.createSignature(
47 | sha256(data),
48 | this.privateKey
49 | );
50 |
51 | return new Uint8Array([
52 | ...(signature.r(32) as any),
53 | ...(signature.s(32) as any),
54 | ]);
55 | };
56 |
57 | verifySignature = async (
58 | data: Uint8Array,
59 | signature: Uint8Array
60 | ): Promise => {
61 | return Secp256k1.verifySignature(
62 | Secp256k1Signature.fromFixedLength(signature),
63 | sha256(data),
64 | this.publicKey
65 | );
66 | };
67 | }
68 |
--------------------------------------------------------------------------------
/src/wallet/ledger/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ledger';
2 |
--------------------------------------------------------------------------------
/src/wallet/ledger/ledger.ts:
--------------------------------------------------------------------------------
1 | import { Signer } from '../signer';
2 | import { LedgerConnector } from '@cosmjs/ledger-amino';
3 | import { defaultAddressPrefix, generateHDPath } from '../utility';
4 | import { HdPath, Secp256k1, Secp256k1Signature, sha256 } from '@cosmjs/crypto';
5 | import { encodeSecp256k1Pubkey, pubkeyToAddress } from '@cosmjs/amino';
6 |
7 | /**
8 | * LedgerSigner implements the logic for the Ledger device signer
9 | */
10 | export class LedgerSigner implements Signer {
11 | private readonly connector: LedgerConnector;
12 | private readonly hdPath: HdPath;
13 | private readonly addressPrefix: string;
14 |
15 | /**
16 | * Creates a new Ledger device signer instance
17 | * @param {LedgerConnector} connector the Ledger connector
18 | * @param {number} accountIndex the desired account index
19 | * @param {string} addressPrefix the address prefix
20 | */
21 | constructor(
22 | connector: LedgerConnector,
23 | accountIndex: number,
24 | addressPrefix: string = defaultAddressPrefix
25 | ) {
26 | this.connector = connector;
27 | this.hdPath = generateHDPath(accountIndex);
28 | this.addressPrefix = addressPrefix;
29 | }
30 |
31 | getAddress = async (): Promise => {
32 | if (!this.connector) {
33 | throw new Error('Ledger not connected');
34 | }
35 |
36 | const compressedPubKey: Uint8Array = await this.connector.getPubkey(
37 | this.hdPath
38 | );
39 |
40 | return pubkeyToAddress(
41 | encodeSecp256k1Pubkey(compressedPubKey),
42 | this.addressPrefix
43 | );
44 | };
45 |
46 | getPublicKey = async (): Promise => {
47 | if (!this.connector) {
48 | throw new Error('Ledger not connected');
49 | }
50 |
51 | return this.connector.getPubkey(this.hdPath);
52 | };
53 |
54 | getPrivateKey = async (): Promise => {
55 | throw new Error('Ledger does not support private key exports');
56 | };
57 |
58 | signData = async (data: Uint8Array): Promise => {
59 | if (!this.connector) {
60 | throw new Error('Ledger not connected');
61 | }
62 |
63 | return this.connector.sign(data, this.hdPath);
64 | };
65 |
66 | verifySignature = async (
67 | data: Uint8Array,
68 | signature: Uint8Array
69 | ): Promise => {
70 | const publicKey = await this.getPublicKey();
71 |
72 | return Secp256k1.verifySignature(
73 | Secp256k1Signature.fromFixedLength(signature),
74 | sha256(data),
75 | publicKey
76 | );
77 | };
78 | }
79 |
--------------------------------------------------------------------------------
/src/wallet/signer.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Signer is the base signer API.
3 | * The signer manages data signing
4 | */
5 | export interface Signer {
6 | /**
7 | * Returns the address associated with the signer's public key
8 | */
9 | getAddress(): Promise;
10 |
11 | /**
12 | * Returns the signer's Secp256k1-compressed public key
13 | */
14 | getPublicKey(): Promise;
15 |
16 | /**
17 | * Returns the signer's actual raw private key
18 | */
19 | getPrivateKey(): Promise;
20 |
21 | /**
22 | * Generates a data signature for arbitrary input
23 | * @param {Uint8Array} data the data to be signed
24 | */
25 | signData(data: Uint8Array): Promise;
26 |
27 | /**
28 | * Verifies if the signature matches the provided raw data
29 | * @param {Uint8Array} data the raw data (not-hashed)
30 | * @param {Uint8Array} signature the hashed-data signature
31 | */
32 | verifySignature(data: Uint8Array, signature: Uint8Array): Promise;
33 | }
34 |
--------------------------------------------------------------------------------
/src/wallet/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from './sign';
2 | export * from './wallet';
3 |
--------------------------------------------------------------------------------
/src/wallet/types/sign.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * The transaction payload that is signed to generate
3 | * a valid transaction signature
4 | */
5 | export interface TxSignPayload {
6 | // the ID of the chain
7 | chain_id: string;
8 | // the account number of the
9 | // account that's signing (decimal)
10 | account_number: string;
11 | // the sequence number of the
12 | // account that's signing (decimal)
13 | sequence: string;
14 | // the fee of the transaction
15 | fee: {
16 | // gas price of the transaction
17 | // in the format
18 | gas_fee: string;
19 | // gas limit of the transaction (decimal)
20 | gas_wanted: string;
21 | };
22 | // the messages associated
23 | // with the transaction.
24 | // These messages have the form: \
25 | // @type: ...
26 | // value: ...
27 | msgs: any[];
28 | // the transaction memo
29 | memo: string;
30 | }
31 |
32 | export const Secp256k1PubKeyType = '/tm.PubKeySecp256k1';
33 |
--------------------------------------------------------------------------------
/src/wallet/types/wallet.ts:
--------------------------------------------------------------------------------
1 | export interface CreateWalletOptions {
2 | // the address prefix
3 | addressPrefix?: string;
4 | // the requested account index
5 | accountIndex?: number;
6 | }
7 |
8 | export type AccountWalletOption = Pick;
9 |
--------------------------------------------------------------------------------
/src/wallet/utility/index.ts:
--------------------------------------------------------------------------------
1 | export * from './utility';
2 |
--------------------------------------------------------------------------------
/src/wallet/utility/utility.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Bip39,
3 | EnglishMnemonic,
4 | HdPath,
5 | Secp256k1,
6 | Slip10,
7 | Slip10Curve,
8 | Slip10RawIndex,
9 | } from '@cosmjs/crypto';
10 | import crypto from 'crypto';
11 |
12 | /**
13 | * Generates the HD path, for the specified index, in the form 'm/44'/118'/0'/0/i',
14 | * where 'i' is the account index
15 | * @param {number} [index=0] the account index
16 | */
17 | export const generateHDPath = (index?: number): HdPath => {
18 | return [
19 | Slip10RawIndex.hardened(44),
20 | Slip10RawIndex.hardened(118),
21 | Slip10RawIndex.hardened(0),
22 | Slip10RawIndex.normal(0),
23 | Slip10RawIndex.normal(index ? index : 0),
24 | ];
25 | };
26 |
27 | /**
28 | * Generates random entropy of the specified size (in B)
29 | * @param {number} [size=32] the entropy size in bytes
30 | */
31 | export const generateEntropy = (size?: number): Uint8Array => {
32 | const array = new Uint8Array(size ? size : 32);
33 |
34 | // Generate random data
35 | crypto.randomFillSync(array);
36 |
37 | return array;
38 | };
39 |
40 | interface keyPair {
41 | privateKey: Uint8Array;
42 | publicKey: Uint8Array;
43 | }
44 |
45 | /**
46 | * Generates a new Secp256k1 key-pair using
47 | * the provided English mnemonic and account index
48 | * @param {string} mnemonic the English mnemonic
49 | * @param {number} [accountIndex=0] the account index
50 | */
51 | export const generateKeyPair = async (
52 | mnemonic: string,
53 | accountIndex?: number
54 | ): Promise => {
55 | // Generate the seed
56 | const seed = await Bip39.mnemonicToSeed(new EnglishMnemonic(mnemonic));
57 |
58 | // Derive the private key
59 | const { privkey: privateKey } = Slip10.derivePath(
60 | Slip10Curve.Secp256k1,
61 | seed,
62 | generateHDPath(accountIndex)
63 | );
64 |
65 | // Derive the public key
66 | const { pubkey: publicKey } = await Secp256k1.makeKeypair(privateKey);
67 |
68 | return {
69 | publicKey: publicKey,
70 | privateKey: privateKey,
71 | };
72 | };
73 |
74 | // Address prefix for TM2 networks
75 | export const defaultAddressPrefix = 'g';
76 |
77 | /**
78 | * Encodes a string into a Uint8Array
79 | * @param {string} str the string to be encoded
80 | */
81 | export const stringToUTF8 = (str: string): Uint8Array => {
82 | return new TextEncoder().encode(str);
83 | };
84 |
85 | /**
86 | * Escapes <,>,& in string.
87 | * Golang's json marshaller escapes <,>,& by default.
88 | * https://cs.opensource.google/go/go/+/refs/tags/go1.20.6:src/encoding/json/encode.go;l=46-53
89 | */
90 | export function encodeCharacterSet(data: string) {
91 | return data
92 | .replace(//g, '\\u003e')
94 | .replace(/&/g, '\\u0026');
95 | }
96 |
--------------------------------------------------------------------------------
/src/wallet/wallet.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BroadcastTxSyncResult,
3 | JSONRPCProvider,
4 | Status,
5 | TransactionEndpoint,
6 | } from '../provider';
7 | import { mock } from 'jest-mock-extended';
8 | import { Wallet } from './wallet';
9 | import { EnglishMnemonic, Secp256k1 } from '@cosmjs/crypto';
10 | import {
11 | defaultAddressPrefix,
12 | generateEntropy,
13 | generateKeyPair,
14 | } from './utility';
15 | import { entropyToMnemonic } from '@cosmjs/crypto/build/bip39';
16 | import { KeySigner } from './key';
17 | import { Signer } from './signer';
18 | import { Tx, TxSignature } from '../proto';
19 | import Long from 'long';
20 | import { Secp256k1PubKeyType } from './types';
21 | import { Any } from '../proto/google/protobuf/any';
22 |
23 | describe('Wallet', () => {
24 | test('createRandom', async () => {
25 | const wallet: Wallet = await Wallet.createRandom();
26 |
27 | expect(wallet).not.toBeNull();
28 |
29 | const address: string = await wallet.getAddress();
30 |
31 | expect(address).toHaveLength(40);
32 | });
33 |
34 | test('connect', async () => {
35 | const mockProvider = mock();
36 | const wallet: Wallet = await Wallet.createRandom();
37 |
38 | // Connect the provider
39 | wallet.connect(mockProvider);
40 |
41 | expect(wallet.getProvider()).toBe(mockProvider);
42 | });
43 |
44 | test('fromMnemonic', async () => {
45 | const mnemonic: EnglishMnemonic = new EnglishMnemonic(
46 | 'lens balcony basic cherry half purchase balance soccer solar scissors process eager orchard fatigue rural retire approve crouch repair prepare develop clarify milk suffer'
47 | );
48 | const wallet: Wallet = await Wallet.fromMnemonic(mnemonic.toString());
49 |
50 | expect(wallet).not.toBeNull();
51 |
52 | // Fetch the address
53 | const address: string = await wallet.getAddress();
54 |
55 | expect(address).toBe(
56 | `${defaultAddressPrefix}1vcjvkjdvckprkcpm7l44plrtg83asfu9geaz90`
57 | );
58 | });
59 |
60 | test('fromPrivateKey', async () => {
61 | const { publicKey, privateKey } = await generateKeyPair(
62 | entropyToMnemonic(generateEntropy()),
63 | 0
64 | );
65 | const signer: Signer = new KeySigner(
66 | privateKey,
67 | Secp256k1.compressPubkey(publicKey)
68 | );
69 |
70 | const wallet: Wallet = await Wallet.fromPrivateKey(privateKey);
71 | const walletSigner: Signer = wallet.getSigner();
72 |
73 | expect(wallet).not.toBeNull();
74 | expect(await wallet.getAddress()).toBe(await signer.getAddress());
75 | expect(await walletSigner.getPublicKey()).toEqual(
76 | await signer.getPublicKey()
77 | );
78 | });
79 |
80 | test('getAccountSequence', async () => {
81 | const mockSequence = 5;
82 | const mockProvider = mock();
83 | mockProvider.getAccountSequence.mockResolvedValue(mockSequence);
84 |
85 | const wallet: Wallet = await Wallet.createRandom();
86 | wallet.connect(mockProvider);
87 |
88 | const address: string = await wallet.getAddress();
89 | const sequence: number = await wallet.getAccountSequence();
90 |
91 | expect(mockProvider.getAccountSequence).toHaveBeenCalledWith(
92 | address,
93 | undefined
94 | );
95 | expect(sequence).toBe(mockSequence);
96 | });
97 |
98 | test('getAccountNumber', async () => {
99 | const mockAccountNumber = 10;
100 | const mockProvider = mock();
101 | mockProvider.getAccountNumber.mockResolvedValue(mockAccountNumber);
102 |
103 | const wallet: Wallet = await Wallet.createRandom();
104 | wallet.connect(mockProvider);
105 |
106 | const address: string = await wallet.getAddress();
107 | const accountNumber: number = await wallet.getAccountNumber();
108 |
109 | expect(mockProvider.getAccountNumber).toHaveBeenCalledWith(
110 | address,
111 | undefined
112 | );
113 | expect(accountNumber).toBe(mockAccountNumber);
114 | });
115 |
116 | test('getBalance', async () => {
117 | const mockBalance = 100;
118 | const mockProvider = mock();
119 | mockProvider.getBalance.mockResolvedValue(mockBalance);
120 |
121 | const wallet: Wallet = await Wallet.createRandom();
122 | wallet.connect(mockProvider);
123 |
124 | const address: string = await wallet.getAddress();
125 | const balance: number = await wallet.getBalance();
126 |
127 | expect(mockProvider.getBalance).toHaveBeenCalledWith(address, 'ugnot');
128 | expect(balance).toBe(mockBalance);
129 | });
130 |
131 | test('getGasPrice', async () => {
132 | const mockGasPrice = 1000;
133 | const mockProvider = mock();
134 | mockProvider.getGasPrice.mockResolvedValue(mockGasPrice);
135 |
136 | const wallet: Wallet = await Wallet.createRandom();
137 | wallet.connect(mockProvider);
138 |
139 | const gasPrice: number = await wallet.getGasPrice();
140 |
141 | expect(mockProvider.getGasPrice).toHaveBeenCalled();
142 | expect(gasPrice).toBe(mockGasPrice);
143 | });
144 |
145 | test('estimateGas', async () => {
146 | const mockTxEstimation = 1000;
147 | const mockTx = mock();
148 | const mockProvider = mock();
149 | mockProvider.estimateGas.mockResolvedValue(mockTxEstimation);
150 |
151 | const wallet: Wallet = await Wallet.createRandom();
152 | wallet.connect(mockProvider);
153 |
154 | const estimation: number = await wallet.estimateGas(mockTx);
155 |
156 | expect(mockProvider.estimateGas).toHaveBeenCalledWith(mockTx);
157 | expect(estimation).toBe(mockTxEstimation);
158 | });
159 |
160 | test('signTransaction', async () => {
161 | const mockTx = mock();
162 | mockTx.signatures = [];
163 | mockTx.fee = {
164 | gasFee: '10',
165 | gasWanted: new Long(10),
166 | };
167 | mockTx.messages = [];
168 |
169 | const mockStatus = mock();
170 | mockStatus.node_info = {
171 | version_set: [],
172 | version: '',
173 | net_address: '',
174 | software: '',
175 | channels: '',
176 | monkier: '',
177 | other: {
178 | tx_index: '',
179 | rpc_address: '',
180 | },
181 | network: 'testchain',
182 | };
183 |
184 | const mockProvider = mock();
185 | mockProvider.getStatus.mockResolvedValue(mockStatus);
186 | mockProvider.getAccountNumber.mockResolvedValue(10);
187 | mockProvider.getAccountSequence.mockResolvedValue(10);
188 |
189 | const wallet: Wallet = await Wallet.createRandom();
190 | wallet.connect(mockProvider);
191 |
192 | const emptyDecodeCallback = (_: Any[]): any[] => {
193 | return [];
194 | };
195 | const signedTx: Tx = await wallet.signTransaction(
196 | mockTx,
197 | emptyDecodeCallback
198 | );
199 |
200 | expect(mockProvider.getStatus).toHaveBeenCalled();
201 | expect(mockProvider.getAccountNumber).toHaveBeenCalled();
202 | expect(mockProvider.getAccountSequence).toHaveBeenCalled();
203 |
204 | expect(signedTx.signatures).toHaveLength(1);
205 |
206 | const sig: TxSignature = signedTx.signatures[0];
207 | expect(sig.pubKey?.typeUrl).toBe(Secp256k1PubKeyType);
208 | expect(sig.pubKey?.value).not.toBeNull();
209 | expect(sig.signature).not.toBeNull();
210 | });
211 |
212 | test('sendTransaction', async () => {
213 | const mockTx = mock();
214 | mockTx.signatures = [];
215 | mockTx.fee = {
216 | gasFee: '10',
217 | gasWanted: new Long(10),
218 | };
219 | mockTx.messages = [];
220 | mockTx.memo = '';
221 |
222 | const mockTxHash = 'tx hash';
223 |
224 | const mockStatus = mock();
225 | mockStatus.node_info = {
226 | version_set: [],
227 | version: '',
228 | net_address: '',
229 | software: '',
230 | channels: '',
231 | monkier: '',
232 | other: {
233 | tx_index: '',
234 | rpc_address: '',
235 | },
236 | network: 'testchain',
237 | };
238 |
239 | const mockTransaction: BroadcastTxSyncResult = {
240 | error: null,
241 | data: null,
242 | Log: '',
243 | hash: mockTxHash,
244 | };
245 |
246 | const mockProvider = mock();
247 | mockProvider.getStatus.mockResolvedValue(mockStatus);
248 | mockProvider.getAccountNumber.mockResolvedValue(10);
249 | mockProvider.getAccountSequence.mockResolvedValue(10);
250 | mockProvider.sendTransaction.mockResolvedValue(mockTransaction);
251 |
252 | const wallet: Wallet = await Wallet.createRandom();
253 | wallet.connect(mockProvider);
254 |
255 | const tx: BroadcastTxSyncResult = await wallet.sendTransaction(
256 | mockTx,
257 | TransactionEndpoint.BROADCAST_TX_SYNC
258 | );
259 |
260 | expect(tx.hash).toBe(mockTxHash);
261 | });
262 | });
263 |
--------------------------------------------------------------------------------
/src/wallet/wallet.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BroadcastTransactionMap,
3 | Provider,
4 | Status,
5 | uint8ArrayToBase64,
6 | } from '../provider';
7 | import { Signer } from './signer';
8 | import { LedgerSigner } from './ledger';
9 | import { KeySigner } from './key';
10 | import { Secp256k1 } from '@cosmjs/crypto';
11 | import {
12 | encodeCharacterSet,
13 | generateEntropy,
14 | generateKeyPair,
15 | stringToUTF8,
16 | } from './utility';
17 | import { LedgerConnector } from '@cosmjs/ledger-amino';
18 | import { entropyToMnemonic } from '@cosmjs/crypto/build/bip39';
19 | import { Any, PubKeySecp256k1, Tx, TxSignature } from '../proto';
20 | import {
21 | AccountWalletOption,
22 | CreateWalletOptions,
23 | Secp256k1PubKeyType,
24 | TxSignPayload,
25 | } from './types';
26 | import { sortedJsonStringify } from '@cosmjs/amino/build/signdoc';
27 |
28 | /**
29 | * Wallet is a single account abstraction
30 | * that can interact with the blockchain
31 | */
32 | export class Wallet {
33 | protected provider: Provider;
34 | protected signer: Signer;
35 |
36 | /**
37 | * Connects the wallet to the specified {@link Provider}
38 | * @param {Provider} provider the active {@link Provider}, if any
39 | */
40 | connect = (provider: Provider) => {
41 | this.provider = provider;
42 | };
43 |
44 | // Wallet initialization //
45 |
46 | /**
47 | * Generates a private key-based wallet, using a random seed
48 | * @param {AccountWalletOption} options the account options
49 | */
50 | static createRandom = async (
51 | options?: AccountWalletOption
52 | ): Promise => {
53 | const { publicKey, privateKey } = await generateKeyPair(
54 | entropyToMnemonic(generateEntropy()),
55 | 0
56 | );
57 |
58 | // Initialize the wallet
59 | const wallet: Wallet = new Wallet();
60 | wallet.signer = new KeySigner(
61 | privateKey,
62 | Secp256k1.compressPubkey(publicKey),
63 | options?.addressPrefix
64 | );
65 |
66 | return wallet;
67 | };
68 |
69 | /**
70 | * Generates a custom signer-based wallet
71 | * @param {Signer} signer the custom signer implementing the Signer interface
72 | * @param {CreateWalletOptions} options the wallet generation options
73 | */
74 | static fromSigner = async (signer: Signer): Promise => {
75 | // Initialize the wallet
76 | const wallet: Wallet = new Wallet();
77 | wallet.signer = signer;
78 |
79 | return wallet;
80 | };
81 |
82 | /**
83 | * Generates a bip39 mnemonic-based wallet
84 | * @param {string} mnemonic the bip39 mnemonic
85 | * @param {CreateWalletOptions} options the wallet generation options
86 | */
87 | static fromMnemonic = async (
88 | mnemonic: string,
89 | options?: CreateWalletOptions
90 | ): Promise