@aboutcircles/sdk-rpc 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +169 -0
- package/dist/client.d.ts +58 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +194 -0
- package/dist/errors.d.ts +44 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +63 -0
- package/dist/events/index.d.ts +8 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/index.js +8 -0
- package/dist/events/observable.d.ts +23 -0
- package/dist/events/observable.d.ts.map +1 -0
- package/dist/events/observable.js +37 -0
- package/dist/events/parser.d.ts +10 -0
- package/dist/events/parser.d.ts.map +1 -0
- package/dist/events/parser.js +103 -0
- package/dist/events/types.d.ts +7 -0
- package/dist/events/types.d.ts.map +1 -0
- package/dist/events/types.js +11 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2654 -0
- package/dist/methods/avatar.d.ts +51 -0
- package/dist/methods/avatar.d.ts.map +1 -0
- package/dist/methods/avatar.js +64 -0
- package/dist/methods/balance.d.ts +37 -0
- package/dist/methods/balance.d.ts.map +1 -0
- package/dist/methods/balance.js +50 -0
- package/dist/methods/group.d.ts +145 -0
- package/dist/methods/group.d.ts.map +1 -0
- package/dist/methods/group.js +380 -0
- package/dist/methods/index.d.ts +11 -0
- package/dist/methods/index.d.ts.map +1 -0
- package/dist/methods/index.js +10 -0
- package/dist/methods/invitation.d.ts +61 -0
- package/dist/methods/invitation.d.ts.map +1 -0
- package/dist/methods/invitation.js +230 -0
- package/dist/methods/pathfinder.d.ts +41 -0
- package/dist/methods/pathfinder.d.ts.map +1 -0
- package/dist/methods/pathfinder.js +53 -0
- package/dist/methods/profile.d.ts +109 -0
- package/dist/methods/profile.d.ts.map +1 -0
- package/dist/methods/profile.js +166 -0
- package/dist/methods/query.d.ts +79 -0
- package/dist/methods/query.d.ts.map +1 -0
- package/dist/methods/query.js +87 -0
- package/dist/methods/token.d.ts +61 -0
- package/dist/methods/token.d.ts.map +1 -0
- package/dist/methods/token.js +99 -0
- package/dist/methods/transaction.d.ts +41 -0
- package/dist/methods/transaction.d.ts.map +1 -0
- package/dist/methods/transaction.js +111 -0
- package/dist/methods/trust.d.ts +114 -0
- package/dist/methods/trust.d.ts.map +1 -0
- package/dist/methods/trust.js +245 -0
- package/dist/pagedQuery.d.ts +106 -0
- package/dist/pagedQuery.d.ts.map +1 -0
- package/dist/pagedQuery.js +254 -0
- package/dist/rpc.d.ts +61 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +76 -0
- package/dist/types.d.ts +82 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +27 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +111 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# @aboutcircles/sdk-rpc
|
|
2
|
+
|
|
3
|
+
TypeScript wrapper for Circles RPC methods.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun install
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { CirclesRpc } from '@aboutcircles/sdk-rpc';
|
|
15
|
+
|
|
16
|
+
// Create RPC instance with default endpoint (https://rpc.circlesubi.network/)
|
|
17
|
+
const rpc = new CirclesRpc();
|
|
18
|
+
|
|
19
|
+
// Or use custom RPC endpoint
|
|
20
|
+
const rpc = new CirclesRpc('https://rpc.aboutcircles.com/');
|
|
21
|
+
|
|
22
|
+
// Note: All addresses are automatically normalized to lowercase
|
|
23
|
+
// since the Circles indexer stores addresses in lowercase
|
|
24
|
+
|
|
25
|
+
// Note: All numeric values (balances, amounts, timestamps) are returned as bigint
|
|
26
|
+
const balance = await rpc.circlesV2.getTotalBalance('0xcadd4ea3bcc361fc4af2387937d7417be8d7dfc2');
|
|
27
|
+
console.log(balance); // 1000000000000000000n
|
|
28
|
+
|
|
29
|
+
// Find a path between two addresses
|
|
30
|
+
const path = await rpc.circlesV2.findPath({
|
|
31
|
+
from: '0x749c930256b47049cb65adcd7c25e72d5de44b3b',
|
|
32
|
+
to: '0xde374ece6fa50e781e81aac78e811b33d16912c7',
|
|
33
|
+
targetFlow: 99999999999999999999999999999999999n
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Query trust relations
|
|
37
|
+
const trustRelations = await rpc.query.query({
|
|
38
|
+
Namespace: 'V_CrcV2',
|
|
39
|
+
Table: 'TrustRelations',
|
|
40
|
+
Columns: [],
|
|
41
|
+
Filter: [{
|
|
42
|
+
Type: 'Conjunction',
|
|
43
|
+
ConjunctionType: 'Or',
|
|
44
|
+
Predicates: [
|
|
45
|
+
{
|
|
46
|
+
Type: 'FilterPredicate',
|
|
47
|
+
FilterType: 'Equals',
|
|
48
|
+
Column: 'truster',
|
|
49
|
+
Value: '0xae3a29a9ff24d0e936a5579bae5c4179c4dff565'
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
Type: 'FilterPredicate',
|
|
53
|
+
FilterType: 'Equals',
|
|
54
|
+
Column: 'trustee',
|
|
55
|
+
Value: '0xae3a29a9ff24d0e936a5579bae5c4179c4dff565'
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}],
|
|
59
|
+
Order: []
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Get common trust
|
|
63
|
+
const commonTrust = await rpc.trust.getCommonTrust(
|
|
64
|
+
'0xde374ece6fa50e781e81aac78e811b33d16912c7',
|
|
65
|
+
'0xe8fc7a2d0573e5164597b05f14fa9a7fca7b215c'
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// Get token balances
|
|
69
|
+
const balances = await rpc.balance.getTokenBalances('0x7cadf434b692ca029d950607a4b3f139c30d4e98');
|
|
70
|
+
// Each balance includes:
|
|
71
|
+
// - attoCircles, staticAttoCircles, attoCrc (as bigint)
|
|
72
|
+
// - circles, staticCircles, crc (as number)
|
|
73
|
+
// - token type flags (isErc20, isErc1155, isWrapped, etc.)
|
|
74
|
+
|
|
75
|
+
// Get avatar info
|
|
76
|
+
const avatarInfo = await rpc.avatar.getAvatarInfo('0xde374ece6fa50e781e81aac78e811b33d16912c7');
|
|
77
|
+
|
|
78
|
+
// Get network snapshot
|
|
79
|
+
const snapshot = await rpc.avatar.getNetworkSnapshot();
|
|
80
|
+
|
|
81
|
+
// Get profile by address
|
|
82
|
+
const profile = await rpc.profile.getProfileByAddress('0xc3a1428c04c426cdf513c6fc8e09f55ddaf50cd7');
|
|
83
|
+
|
|
84
|
+
// Get profiles by address batch
|
|
85
|
+
const profiles = await rpc.profile.getProfileByAddressBatch([
|
|
86
|
+
'0xc3a1428c04c426cdf513c6fc8e09f55ddaf50cd7',
|
|
87
|
+
'0xf712d3b31de494b5c0ea51a6a407460ca66b12e8'
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
// Search profiles
|
|
91
|
+
const results = await rpc.profile.searchProfiles('alice', 10, 0);
|
|
92
|
+
|
|
93
|
+
// Get available tables
|
|
94
|
+
const tables = await rpc.query.tables();
|
|
95
|
+
|
|
96
|
+
// Query events
|
|
97
|
+
const events = await rpc.query.events(
|
|
98
|
+
38000000,
|
|
99
|
+
null,
|
|
100
|
+
['CrcV1_Trust'],
|
|
101
|
+
null,
|
|
102
|
+
false
|
|
103
|
+
);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## API
|
|
107
|
+
|
|
108
|
+
**Note:** All balance and amount values are returned as `bigint` for safe handling of large numbers.
|
|
109
|
+
|
|
110
|
+
### CirclesV2 Methods
|
|
111
|
+
|
|
112
|
+
- `getTotalBalanceV1(address, asTimeCircles?): Promise<bigint>` - Get total v1 Circles balance
|
|
113
|
+
- `getTotalBalance(address, asTimeCircles?): Promise<bigint>` - Get total v2 Circles balance
|
|
114
|
+
- `findPath(params): Promise<PathResponse>` - Calculate transfer path between addresses
|
|
115
|
+
- **params**: `{ from, to, targetFlow, useWrappedBalances?, fromTokens?, toTokens?, excludeFromTokens?, excludeToTokens?, simulatedBalances? }`
|
|
116
|
+
- All amounts are `bigint`, addresses normalized to lowercase
|
|
117
|
+
- Returns path with `flow` and `transfers` (all amounts as `bigint`)
|
|
118
|
+
|
|
119
|
+
### Query Methods
|
|
120
|
+
|
|
121
|
+
- `query(params)` - Query tables with filters
|
|
122
|
+
- `tables()` - Get available namespaces and tables
|
|
123
|
+
- `events(fromBlock, toBlock, eventTypes, address, includeTransactionData)` - Query events
|
|
124
|
+
|
|
125
|
+
### Trust Methods
|
|
126
|
+
|
|
127
|
+
- `getCommonTrust(address1, address2)` - Get common trust relations
|
|
128
|
+
|
|
129
|
+
### Balance Methods
|
|
130
|
+
|
|
131
|
+
- `getTokenBalances(address): Promise<TokenBalance[]>` - Get token balance breakdown
|
|
132
|
+
- Returns balances with raw amounts as `bigint` (attoCircles, staticAttoCircles, attoCrc)
|
|
133
|
+
- And converted amounts as `number` (circles, staticCircles, crc)
|
|
134
|
+
- Includes token type flags (isErc20, isErc1155, isWrapped, isInflationary, isGroup)
|
|
135
|
+
|
|
136
|
+
### Avatar Methods
|
|
137
|
+
|
|
138
|
+
- `getAvatarInfo(address): Promise<AvatarInfo | undefined>` - Get avatar information
|
|
139
|
+
- `getAvatarInfoBatch(addresses[]): Promise<AvatarInfo[]>` - Get multiple avatar infos in batch
|
|
140
|
+
- `getNetworkSnapshot(): Promise<NetworkSnapshot>` - Get full network snapshot
|
|
141
|
+
|
|
142
|
+
### Profile Methods
|
|
143
|
+
|
|
144
|
+
- `getProfileByCid(cid): Promise<Profile | null>` - Get profile by CID
|
|
145
|
+
- `getProfileByCidBatch(cids[]): Promise<(Profile | null)[]>` - Get multiple profiles by CID
|
|
146
|
+
- `getProfileByAddress(address): Promise<Profile | null>` - Get profile by address
|
|
147
|
+
- `getProfileByAddressBatch(addresses[]): Promise<(Profile | null)[]>` - Get multiple profiles by address
|
|
148
|
+
- `searchProfiles(query, limit?, offset?): Promise<Profile[]>` - Search profiles
|
|
149
|
+
|
|
150
|
+
### Token Methods
|
|
151
|
+
|
|
152
|
+
- `getTokenInfo(address): Promise<TokenInfo | undefined>` - Get token information
|
|
153
|
+
- `getTokenInfoBatch(addresses[]): Promise<TokenInfo[]>` - Get multiple token infos in batch
|
|
154
|
+
|
|
155
|
+
### Invitation Methods
|
|
156
|
+
|
|
157
|
+
- `getInvitedBy(address): Promise<Address | undefined>` - Get the avatar that invited a specific address
|
|
158
|
+
|
|
159
|
+
## Build
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
bun run build
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Development
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
bun run dev
|
|
169
|
+
```
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Address } from '@aboutcircles/sdk-types';
|
|
2
|
+
import { Observable } from './events';
|
|
3
|
+
import type { CirclesEvent } from './events';
|
|
4
|
+
/**
|
|
5
|
+
* Base RPC client for making JSON-RPC calls to Circles RPC endpoints
|
|
6
|
+
* Supports both HTTP requests and WebSocket subscriptions
|
|
7
|
+
*/
|
|
8
|
+
export declare class RpcClient {
|
|
9
|
+
private rpcUrl;
|
|
10
|
+
private requestId;
|
|
11
|
+
private websocket;
|
|
12
|
+
private websocketConnected;
|
|
13
|
+
private pendingResponses;
|
|
14
|
+
private subscriptionListeners;
|
|
15
|
+
private reconnectAttempt;
|
|
16
|
+
private readonly initialBackoff;
|
|
17
|
+
private readonly maxBackoff;
|
|
18
|
+
constructor(rpcUrl: string);
|
|
19
|
+
/**
|
|
20
|
+
* Make a JSON-RPC call
|
|
21
|
+
*/
|
|
22
|
+
call<TParams = unknown[], TResult = unknown>(method: string, params: TParams): Promise<TResult>;
|
|
23
|
+
/**
|
|
24
|
+
* Update the RPC URL
|
|
25
|
+
*/
|
|
26
|
+
setRpcUrl(rpcUrl: string): void;
|
|
27
|
+
/**
|
|
28
|
+
* Get the current RPC URL
|
|
29
|
+
*/
|
|
30
|
+
getRpcUrl(): string;
|
|
31
|
+
/**
|
|
32
|
+
* Initiates a WebSocket connection
|
|
33
|
+
* @private
|
|
34
|
+
*/
|
|
35
|
+
private connect;
|
|
36
|
+
/**
|
|
37
|
+
* Schedules a reconnect using exponential backoff with random jitter
|
|
38
|
+
* @private
|
|
39
|
+
*/
|
|
40
|
+
private scheduleReconnect;
|
|
41
|
+
/**
|
|
42
|
+
* Attempts to reconnect the WebSocket
|
|
43
|
+
* @private
|
|
44
|
+
*/
|
|
45
|
+
private reconnect;
|
|
46
|
+
/**
|
|
47
|
+
* Sends a message over WebSocket
|
|
48
|
+
* @private
|
|
49
|
+
*/
|
|
50
|
+
private sendMessage;
|
|
51
|
+
/**
|
|
52
|
+
* Subscribe to Circles events via WebSocket
|
|
53
|
+
* @param address Optional address to filter events for a specific avatar
|
|
54
|
+
* @returns Observable that emits CirclesEvent objects
|
|
55
|
+
*/
|
|
56
|
+
subscribe(address?: Address): Promise<Observable<CirclesEvent>>;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAmC,OAAO,EAAE,MAAM,yBAAyB,CAAC;AACxF,OAAO,EAAE,UAAU,EAA+B,MAAM,UAAU,CAAC;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAG7C;;;GAGG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAAa;IAG9B,OAAO,CAAC,SAAS,CAA0B;IAC3C,OAAO,CAAC,kBAAkB,CAAS;IACnC,OAAO,CAAC,gBAAgB,CAA2B;IACnD,OAAO,CAAC,qBAAqB,CAEtB;IAGP,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAQ;IACvC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAU;gBAEzB,MAAM,EAAE,MAAM;IAI1B;;OAEG;IACG,IAAI,CAAC,OAAO,GAAG,OAAO,EAAE,EAAE,OAAO,GAAG,OAAO,EAC/C,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,OAAO,GACd,OAAO,CAAC,OAAO,CAAC;IA6CnB;;OAEG;IACH,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAI/B;;OAEG;IACH,SAAS,IAAI,MAAM;IAInB;;;OAGG;IACH,OAAO,CAAC,OAAO;IAiDf;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAqBzB;;;OAGG;YACW,SAAS;IAWvB;;;OAGG;IACH,OAAO,CAAC,WAAW;IAmBnB;;;;OAIG;IACG,SAAS,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;CAsBtE"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { Observable, parseRpcSubscriptionMessage } from './events';
|
|
2
|
+
import { RpcError } from './errors';
|
|
3
|
+
/**
|
|
4
|
+
* Base RPC client for making JSON-RPC calls to Circles RPC endpoints
|
|
5
|
+
* Supports both HTTP requests and WebSocket subscriptions
|
|
6
|
+
*/
|
|
7
|
+
export class RpcClient {
|
|
8
|
+
rpcUrl;
|
|
9
|
+
requestId = 0;
|
|
10
|
+
// WebSocket fields
|
|
11
|
+
websocket = null;
|
|
12
|
+
websocketConnected = false;
|
|
13
|
+
pendingResponses = {};
|
|
14
|
+
subscriptionListeners = {};
|
|
15
|
+
// Backoff-related fields for reconnection
|
|
16
|
+
reconnectAttempt = 0;
|
|
17
|
+
initialBackoff = 2000; // 2 seconds
|
|
18
|
+
maxBackoff = 120000; // 2 minutes
|
|
19
|
+
constructor(rpcUrl) {
|
|
20
|
+
this.rpcUrl = rpcUrl;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Make a JSON-RPC call
|
|
24
|
+
*/
|
|
25
|
+
async call(method, params) {
|
|
26
|
+
this.requestId++;
|
|
27
|
+
const request = {
|
|
28
|
+
jsonrpc: '2.0',
|
|
29
|
+
id: this.requestId,
|
|
30
|
+
method,
|
|
31
|
+
params,
|
|
32
|
+
};
|
|
33
|
+
try {
|
|
34
|
+
const response = await fetch(this.rpcUrl, {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: {
|
|
37
|
+
'Content-Type': 'application/json',
|
|
38
|
+
},
|
|
39
|
+
body: JSON.stringify(request),
|
|
40
|
+
});
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
throw RpcError.connectionFailed(this.rpcUrl, new Error(`HTTP ${response.status}: ${response.statusText}`));
|
|
43
|
+
}
|
|
44
|
+
const jsonResponse = await response.json();
|
|
45
|
+
if (jsonResponse.error) {
|
|
46
|
+
throw RpcError.fromJsonRpcError(jsonResponse.error);
|
|
47
|
+
}
|
|
48
|
+
if (jsonResponse.result === undefined) {
|
|
49
|
+
throw RpcError.invalidResponse(method, jsonResponse);
|
|
50
|
+
}
|
|
51
|
+
return jsonResponse.result;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (error instanceof RpcError) {
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
throw RpcError.connectionFailed(this.rpcUrl, error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Update the RPC URL
|
|
62
|
+
*/
|
|
63
|
+
setRpcUrl(rpcUrl) {
|
|
64
|
+
this.rpcUrl = rpcUrl;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Get the current RPC URL
|
|
68
|
+
*/
|
|
69
|
+
getRpcUrl() {
|
|
70
|
+
return this.rpcUrl;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Initiates a WebSocket connection
|
|
74
|
+
* @private
|
|
75
|
+
*/
|
|
76
|
+
connect() {
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
let wsUrl = this.rpcUrl.replace('http', 'ws');
|
|
79
|
+
if (wsUrl.endsWith('/')) {
|
|
80
|
+
wsUrl += 'ws';
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
wsUrl += '/ws';
|
|
84
|
+
}
|
|
85
|
+
this.websocket = new WebSocket(wsUrl);
|
|
86
|
+
this.websocket.onopen = () => {
|
|
87
|
+
console.log('WebSocket connected');
|
|
88
|
+
this.websocketConnected = true;
|
|
89
|
+
// Reset the reconnect backoff attempts
|
|
90
|
+
this.reconnectAttempt = 0;
|
|
91
|
+
resolve();
|
|
92
|
+
};
|
|
93
|
+
this.websocket.onmessage = (event) => {
|
|
94
|
+
const message = JSON.parse(event.data);
|
|
95
|
+
const { id, method, params } = message;
|
|
96
|
+
if (id !== undefined && this.pendingResponses[id]) {
|
|
97
|
+
this.pendingResponses[id].resolve(message);
|
|
98
|
+
delete this.pendingResponses[id];
|
|
99
|
+
}
|
|
100
|
+
if (method === 'eth_subscription' && params) {
|
|
101
|
+
const { subscription, result } = params;
|
|
102
|
+
if (this.subscriptionListeners[subscription]) {
|
|
103
|
+
this.subscriptionListeners[subscription].forEach(listener => listener(result));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
this.websocket.onclose = () => {
|
|
108
|
+
console.warn('WebSocket closed');
|
|
109
|
+
this.websocketConnected = false;
|
|
110
|
+
};
|
|
111
|
+
this.websocket.onerror = (error) => {
|
|
112
|
+
console.error('WebSocket error:', error);
|
|
113
|
+
this.websocketConnected = false;
|
|
114
|
+
// Schedule a reconnect
|
|
115
|
+
this.scheduleReconnect();
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Schedules a reconnect using exponential backoff with random jitter
|
|
121
|
+
* @private
|
|
122
|
+
*/
|
|
123
|
+
scheduleReconnect() {
|
|
124
|
+
// Exponential backoff: 2^attempt * initialBackoff
|
|
125
|
+
const delay = Math.min(this.initialBackoff * Math.pow(2, this.reconnectAttempt), this.maxBackoff);
|
|
126
|
+
// Add proportional jitter (between 0% and 50% of the delay)
|
|
127
|
+
const jitter = delay * (Math.random() * 0.5);
|
|
128
|
+
const timeout = delay + jitter;
|
|
129
|
+
console.log(`Reconnecting in ${Math.round(timeout)}ms (attempt #${this.reconnectAttempt + 1})`);
|
|
130
|
+
this.reconnectAttempt++;
|
|
131
|
+
setTimeout(() => {
|
|
132
|
+
this.reconnect();
|
|
133
|
+
}, timeout);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Attempts to reconnect the WebSocket
|
|
137
|
+
* @private
|
|
138
|
+
*/
|
|
139
|
+
async reconnect() {
|
|
140
|
+
if (this.websocketConnected)
|
|
141
|
+
return;
|
|
142
|
+
try {
|
|
143
|
+
await this.connect();
|
|
144
|
+
console.log('Reconnection successful');
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
console.error('Reconnection attempt failed:', err);
|
|
148
|
+
this.scheduleReconnect();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Sends a message over WebSocket
|
|
153
|
+
* @private
|
|
154
|
+
*/
|
|
155
|
+
sendMessage(method, params, timeout = 5000) {
|
|
156
|
+
if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) {
|
|
157
|
+
return Promise.reject(RpcError.connectionFailed(this.rpcUrl));
|
|
158
|
+
}
|
|
159
|
+
const id = this.requestId++;
|
|
160
|
+
const message = { jsonrpc: '2.0', method, params, id };
|
|
161
|
+
return new Promise((resolve, reject) => {
|
|
162
|
+
this.pendingResponses[id] = { resolve, reject };
|
|
163
|
+
this.websocket.send(JSON.stringify(message));
|
|
164
|
+
setTimeout(() => {
|
|
165
|
+
if (this.pendingResponses[id]) {
|
|
166
|
+
this.pendingResponses[id].reject(RpcError.timeout(method, timeout));
|
|
167
|
+
delete this.pendingResponses[id];
|
|
168
|
+
}
|
|
169
|
+
}, timeout);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Subscribe to Circles events via WebSocket
|
|
174
|
+
* @param address Optional address to filter events for a specific avatar
|
|
175
|
+
* @returns Observable that emits CirclesEvent objects
|
|
176
|
+
*/
|
|
177
|
+
async subscribe(address) {
|
|
178
|
+
const normalizedAddress = address?.toLowerCase();
|
|
179
|
+
if (!this.websocketConnected) {
|
|
180
|
+
await this.connect();
|
|
181
|
+
}
|
|
182
|
+
const observable = Observable.create();
|
|
183
|
+
const subscriptionArgs = JSON.stringify(normalizedAddress ? { address: normalizedAddress } : {});
|
|
184
|
+
const response = await this.sendMessage('eth_subscribe', ['circles', subscriptionArgs]);
|
|
185
|
+
const subscriptionId = response.result;
|
|
186
|
+
if (!this.subscriptionListeners[subscriptionId]) {
|
|
187
|
+
this.subscriptionListeners[subscriptionId] = [];
|
|
188
|
+
}
|
|
189
|
+
this.subscriptionListeners[subscriptionId].push((events) => {
|
|
190
|
+
parseRpcSubscriptionMessage(events).forEach(event => observable.emit(event));
|
|
191
|
+
});
|
|
192
|
+
return observable.property;
|
|
193
|
+
}
|
|
194
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC Package Error Types
|
|
3
|
+
*/
|
|
4
|
+
import { CirclesError } from '@aboutcircles/sdk-utils';
|
|
5
|
+
/**
|
|
6
|
+
* RPC-specific error sources
|
|
7
|
+
*/
|
|
8
|
+
export type RpcErrorSource = 'RPC_CONNECTION' | 'RPC_REQUEST' | 'RPC_RESPONSE' | 'RPC_TIMEOUT' | 'RPC_WEBSOCKET';
|
|
9
|
+
/**
|
|
10
|
+
* RPC-related errors
|
|
11
|
+
*/
|
|
12
|
+
export declare class RpcError extends CirclesError<RpcErrorSource> {
|
|
13
|
+
constructor(message: string, options?: {
|
|
14
|
+
code?: string | number;
|
|
15
|
+
source?: RpcErrorSource;
|
|
16
|
+
cause?: unknown;
|
|
17
|
+
context?: Record<string, any>;
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* Create error for connection failures
|
|
21
|
+
*/
|
|
22
|
+
static connectionFailed(url: string, cause?: unknown): RpcError;
|
|
23
|
+
/**
|
|
24
|
+
* Create error for timeout
|
|
25
|
+
*/
|
|
26
|
+
static timeout(method: string, timeout: number): RpcError;
|
|
27
|
+
/**
|
|
28
|
+
* Create error for invalid response
|
|
29
|
+
*/
|
|
30
|
+
static invalidResponse(method: string, response: any): RpcError;
|
|
31
|
+
/**
|
|
32
|
+
* Create error from JSON-RPC error response
|
|
33
|
+
*/
|
|
34
|
+
static fromJsonRpcError(error: {
|
|
35
|
+
code: number;
|
|
36
|
+
message: string;
|
|
37
|
+
data?: any;
|
|
38
|
+
}): RpcError;
|
|
39
|
+
/**
|
|
40
|
+
* Create error for WebSocket failures
|
|
41
|
+
*/
|
|
42
|
+
static websocketError(message: string, cause?: unknown): RpcError;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAEvD;;GAEG;AACH,MAAM,MAAM,cAAc,GACtB,gBAAgB,GAChB,aAAa,GACb,cAAc,GACd,aAAa,GACb,eAAe,CAAC;AAEpB;;GAEG;AACH,qBAAa,QAAS,SAAQ,YAAY,CAAC,cAAc,CAAC;gBAEtD,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;QACR,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QACvB,MAAM,CAAC,EAAE,cAAc,CAAC;QACxB,KAAK,CAAC,EAAE,OAAO,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;KAC/B;IAKH;;OAEG;IACH,MAAM,CAAC,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,QAAQ;IAS/D;;OAEG;IACH,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,QAAQ;IAQzD;;OAEG;IACH,MAAM,CAAC,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,GAAG,QAAQ;IAQ/D;;OAEG;IACH,MAAM,CAAC,gBAAgB,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,GAAG,CAAA;KAAE,GAAG,QAAQ;IAQvF;;OAEG;IACH,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,QAAQ;CAOlE"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC Package Error Types
|
|
3
|
+
*/
|
|
4
|
+
import { CirclesError } from '@aboutcircles/sdk-utils';
|
|
5
|
+
/**
|
|
6
|
+
* RPC-related errors
|
|
7
|
+
*/
|
|
8
|
+
export class RpcError extends CirclesError {
|
|
9
|
+
constructor(message, options) {
|
|
10
|
+
super('RpcError', message, { ...options, source: options?.source ?? 'RPC_REQUEST' });
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Create error for connection failures
|
|
14
|
+
*/
|
|
15
|
+
static connectionFailed(url, cause) {
|
|
16
|
+
return new RpcError('Failed to connect to RPC endpoint', {
|
|
17
|
+
code: 'RPC_CONNECTION_FAILED',
|
|
18
|
+
source: 'RPC_CONNECTION',
|
|
19
|
+
cause,
|
|
20
|
+
context: { url },
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Create error for timeout
|
|
25
|
+
*/
|
|
26
|
+
static timeout(method, timeout) {
|
|
27
|
+
return new RpcError('RPC request timed out', {
|
|
28
|
+
code: 'RPC_TIMEOUT',
|
|
29
|
+
source: 'RPC_TIMEOUT',
|
|
30
|
+
context: { method, timeout },
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Create error for invalid response
|
|
35
|
+
*/
|
|
36
|
+
static invalidResponse(method, response) {
|
|
37
|
+
return new RpcError('Invalid RPC response', {
|
|
38
|
+
code: 'RPC_INVALID_RESPONSE',
|
|
39
|
+
source: 'RPC_RESPONSE',
|
|
40
|
+
context: { method, response },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Create error from JSON-RPC error response
|
|
45
|
+
*/
|
|
46
|
+
static fromJsonRpcError(error) {
|
|
47
|
+
return new RpcError(error.message, {
|
|
48
|
+
code: error.code,
|
|
49
|
+
source: 'RPC_RESPONSE',
|
|
50
|
+
context: { data: error.data },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Create error for WebSocket failures
|
|
55
|
+
*/
|
|
56
|
+
static websocketError(message, cause) {
|
|
57
|
+
return new RpcError(message, {
|
|
58
|
+
code: 'RPC_WEBSOCKET_ERROR',
|
|
59
|
+
source: 'RPC_WEBSOCKET',
|
|
60
|
+
cause,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event observation and subscription system
|
|
3
|
+
*/
|
|
4
|
+
export type { CirclesEvent, CirclesEventType, CirclesBaseEvent, CirclesEventOfType, RpcSubscriptionEvent, } from './types';
|
|
5
|
+
export { isCirclesEvent } from './types';
|
|
6
|
+
export { parseRpcEvent, parseRpcSubscriptionMessage } from './parser';
|
|
7
|
+
export { Observable } from './observable';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/events/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,YAAY,EACV,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAGzC,OAAO,EAAE,aAAa,EAAE,2BAA2B,EAAE,MAAM,UAAU,CAAC;AAGtE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observable class for event streaming
|
|
3
|
+
* Provides a simple publish-subscribe pattern for Circles events
|
|
4
|
+
*/
|
|
5
|
+
export declare class Observable<TEvent> {
|
|
6
|
+
private readonly _subscribers;
|
|
7
|
+
/**
|
|
8
|
+
* Subscribe to events
|
|
9
|
+
* @param subscriber Callback function to be called for each event
|
|
10
|
+
* @returns Unsubscribe function to stop receiving events
|
|
11
|
+
*/
|
|
12
|
+
subscribe(subscriber: (value: TEvent) => void): () => void;
|
|
13
|
+
protected constructor();
|
|
14
|
+
protected emit(value: TEvent): void;
|
|
15
|
+
/**
|
|
16
|
+
* Create a new Observable with an emitter
|
|
17
|
+
*/
|
|
18
|
+
static create<T>(): {
|
|
19
|
+
property: Observable<T>;
|
|
20
|
+
emit: (e: T) => void;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=observable.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observable.d.ts","sourceRoot":"","sources":["../../src/events/observable.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qBAAa,UAAU,CAAC,MAAM;IAC5B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAmC;IAEhE;;;;OAIG;IACH,SAAS,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI;IAU1D,SAAS;IAIT,SAAS,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM;IAI5B;;OAEG;WACW,MAAM,CAAC,CAAC,KAAK;QAAE,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;QAAC,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,IAAI,CAAA;KAAE;CAO7E"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observable class for event streaming
|
|
3
|
+
* Provides a simple publish-subscribe pattern for Circles events
|
|
4
|
+
*/
|
|
5
|
+
export class Observable {
|
|
6
|
+
_subscribers = [];
|
|
7
|
+
/**
|
|
8
|
+
* Subscribe to events
|
|
9
|
+
* @param subscriber Callback function to be called for each event
|
|
10
|
+
* @returns Unsubscribe function to stop receiving events
|
|
11
|
+
*/
|
|
12
|
+
subscribe(subscriber) {
|
|
13
|
+
this._subscribers.push(subscriber);
|
|
14
|
+
return () => {
|
|
15
|
+
const index = this._subscribers.indexOf(subscriber);
|
|
16
|
+
if (index > -1) {
|
|
17
|
+
this._subscribers.splice(index, 1);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
constructor() {
|
|
22
|
+
this._subscribers = [];
|
|
23
|
+
}
|
|
24
|
+
emit(value) {
|
|
25
|
+
this._subscribers.forEach(sub => sub(value));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Create a new Observable with an emitter
|
|
29
|
+
*/
|
|
30
|
+
static create() {
|
|
31
|
+
const observable = new Observable();
|
|
32
|
+
return {
|
|
33
|
+
property: observable,
|
|
34
|
+
emit: (e) => observable.emit(e)
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CirclesEvent, RpcSubscriptionEvent } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Parse a single RPC subscription event into a CirclesEvent
|
|
4
|
+
*/
|
|
5
|
+
export declare function parseRpcEvent(rpcEvent: RpcSubscriptionEvent): CirclesEvent;
|
|
6
|
+
/**
|
|
7
|
+
* Parse an array of RPC subscription events
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseRpcSubscriptionMessage(message: RpcSubscriptionEvent[]): CirclesEvent[];
|
|
10
|
+
//# sourceMappingURL=parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/events/parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAgFlE;;GAEG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,oBAAoB,GAAG,YAAY,CAwB1E;AAED;;GAEG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,oBAAoB,EAAE,GAAG,YAAY,EAAE,CAE3F"}
|