Skip to content

Commit 920a7b8

Browse files
rafaelcrzone117x
andauthored
feat: send nft updates through websocket channels (#1218)
* feat: draft listener class * refactor: interface draft * feat: socket.io channel * docs: comments * feat: ws-rpc channel * refactor: rename metrics.ts * fix: socket.io tests * fix: websocket tests * fix: unused exports * fix: update on stale mempool tx drop * feat: return complete objects in ws-rpc * test: complete ws-rpc responses * docs: update address tx example * fix: take package.json from matt's branch to fix mobx * fix: also take client and docs pacakge.json * fix: cherry-pick ci * feat: start sending nft event updates * feat: finish socket.io * fix: typo on asset topic * feat: ws-rpc support * feat: update client libs * chore: add client docs, add extra properties * fix: const socket.io subs Co-authored-by: Matthew Little <[email protected]>
1 parent 664cce7 commit 920a7b8

27 files changed

+1045
-20
lines changed

client/README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,52 @@ Subscribe via Socket.io:
415415
sc.subscribeAddressStxBalance('SP3C5SSYVKPAWTR8Y63CVYBR65GD3MG7K80526D1Q');
416416
```
417417

418+
### NFT event updates
419+
420+
Sent every time an NFT event occurs. You can subscribe to all events or events scoped to a single
421+
collection or a single asset.
422+
423+
```json
424+
{
425+
"asset_event_type": "transfer",
426+
"asset_identifier": "SP176ZMV706NZGDDX8VSQRGMB7QN33BBDVZ6BMNHD.project-indigo-act1::Project-Indigo-Act1",
427+
"value": {
428+
"hex": "0x0100000000000000000000000000000095",
429+
"repr": "u149"
430+
},
431+
"tx_id": "0xfb4bfc274007825dfd2d8f6c3f429407016779e9954775f82129108282d4c4ce",
432+
"tx_index": 0,
433+
"sender": null,
434+
"recipient": "SP3BK1NNSWN719Z6KDW05RBGVS940YCN6X84STYPR",
435+
"block_height": 45231,
436+
"event_index": 0,
437+
}
438+
```
439+
Subscribe via WebSockets:
440+
```js
441+
client.subscribeNftEventUpdates(event => {});
442+
client.subscribeNftAssetEventUpdates(
443+
'SP176ZMV706NZGDDX8VSQRGMB7QN33BBDVZ6BMNHD.project-indigo-act1::Project-Indigo-Act1',
444+
'0x0100000000000000000000000000000095',
445+
event => {}
446+
);
447+
client.subscribeNftCollectionEventUpdates(
448+
'SP176ZMV706NZGDDX8VSQRGMB7QN33BBDVZ6BMNHD.project-indigo-act1::Project-Indigo-Act1',
449+
event => {}
450+
);
451+
```
452+
Subscribe via Socket.io:
453+
```js
454+
sc.subscribeNftEventUpdates();
455+
sc.subscribeNftAssetEventUpdates(
456+
'SP176ZMV706NZGDDX8VSQRGMB7QN33BBDVZ6BMNHD.project-indigo-act1::Project-Indigo-Act1',
457+
'0x0100000000000000000000000000000095',
458+
);
459+
sc.subscribeNftCollectionEventUpdates(
460+
'SP176ZMV706NZGDDX8VSQRGMB7QN33BBDVZ6BMNHD.project-indigo-act1::Project-Indigo-Act1',
461+
);
462+
```
463+
418464
## Known Issues
419465

420466
- The TypeScript definitions for several objects involving type unions, including transactions, are incorrectly specified as only `object`.

client/src/socket-io/index.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,30 @@ export class StacksApiSocketClient {
120120
this.handleSubscription(`transaction:${txId}` as const, false);
121121
}
122122

123+
subscribeNftEvent() {
124+
return this.handleSubscription('nft-event', true);
125+
}
126+
127+
unsubscribeNftEvent() {
128+
this.handleSubscription('nft-event', false);
129+
}
130+
131+
subscribeNftAssetEvent(assetIdentifier: string, value: string) {
132+
return this.handleSubscription(`nft-asset-event:${assetIdentifier}+${value}` as const, true);
133+
}
134+
135+
unsubscribeNftAssetEvent(assetIdentifier: string, value: string) {
136+
this.handleSubscription(`nft-asset-event:${assetIdentifier}+${value}` as const, false);
137+
}
138+
139+
subscribeNftCollectionEvent(assetIdentifier: string) {
140+
return this.handleSubscription(`nft-collection-event:${assetIdentifier}` as const, true);
141+
}
142+
143+
unsubscribeNftCollectionEvent(assetIdentifier: string) {
144+
this.handleSubscription(`nft-collection-event:${assetIdentifier}` as const, false);
145+
}
146+
123147
logEvents() {
124148
this.socket.on('connect', () => console.log('socket connected'));
125149
this.socket.on('disconnect', reason => console.warn('disconnected', reason));
@@ -133,5 +157,12 @@ export class StacksApiSocketClient {
133157
this.socket.on('address-stx-balance', (address, data) =>
134158
console.log('address-stx-balance', address, data)
135159
);
160+
this.socket.on('nft-event', event => console.log('nft-event', event));
161+
this.socket.on('nft-asset-event', (assetIdentifier, value, event) =>
162+
console.log('nft-asset-event', assetIdentifier, value, event)
163+
);
164+
this.socket.on('nft-collection-event', (assetIdentifier, event) =>
165+
console.log('nft-collection-event', assetIdentifier, event)
166+
);
136167
}
137168
}

client/src/ws/index.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ import {
1414
MempoolTransaction,
1515
RpcAddressBalanceNotificationParams,
1616
RpcAddressTxNotificationParams,
17+
NftEvent,
18+
RpcNftEventSubscriptionParams,
19+
RpcNftAssetEventSubscriptionParams,
20+
RpcNftCollectionEventSubscriptionParams,
1721
} from '@stacks/stacks-blockchain-api-types';
1822
import { BASE_PATH } from '../generated/runtime';
1923

@@ -38,6 +42,9 @@ export class StacksApiWebSocketClient {
3842
txUpdate: (event: Transaction | MempoolTransaction) => any;
3943
addressTxUpdate: (event: RpcAddressTxNotificationParams) => void;
4044
addressBalanceUpdate: (event: RpcAddressBalanceNotificationParams) => void;
45+
nftEventUpdate: (event: NftEvent) => void;
46+
nftAssetEventUpdate: (event: NftEvent) => void;
47+
nftCollectionEventUpdate: (event: NftEvent) => void;
4148
}>();
4249

4350
public static async connect(url: string = BASE_PATH): Promise<StacksApiWebSocketClient> {
@@ -114,6 +121,15 @@ export class StacksApiWebSocketClient {
114121
case 'mempool':
115122
this.eventEmitter.emit('mempool', data.params as Transaction);
116123
break;
124+
case 'nft_event':
125+
this.eventEmitter.emit('nftEventUpdate', data.params as NftEvent);
126+
break;
127+
case 'nft_asset_event':
128+
this.eventEmitter.emit('nftAssetEventUpdate', data.params as NftEvent);
129+
break;
130+
case 'nft_collection_event':
131+
this.eventEmitter.emit('nftCollectionEventUpdate', data.params as NftEvent);
132+
break;
117133
}
118134
}
119135

@@ -232,6 +248,71 @@ export class StacksApiWebSocketClient {
232248
},
233249
};
234250
}
251+
252+
async subscribeNftEventUpdates(update: (event: NftEvent) => any): Promise<Subscription> {
253+
const params: RpcNftEventSubscriptionParams = {
254+
event: 'nft_event',
255+
};
256+
await this.rpcCall('subscribe', params);
257+
const listener = (event: NftEvent) => {
258+
update(event);
259+
};
260+
this.eventEmitter.addListener('nftEventUpdate', listener);
261+
return {
262+
unsubscribe: () => {
263+
this.eventEmitter.removeListener('nftEventUpdate', listener);
264+
return this.rpcCall('unsubscribe', params);
265+
},
266+
};
267+
}
268+
269+
async subscribeNftAssetEventUpdates(
270+
assetIdentifier: string,
271+
value: string,
272+
update: (event: NftEvent) => any
273+
): Promise<Subscription> {
274+
const params: RpcNftAssetEventSubscriptionParams = {
275+
event: 'nft_asset_event',
276+
asset_identifier: assetIdentifier,
277+
value,
278+
};
279+
const subscribed = await this.rpcCall<{ asset_identifier: string, value: string }>('subscribe', params);
280+
const listener = (event: NftEvent) => {
281+
if (event.asset_identifier === subscribed.asset_identifier && event.value.hex === subscribed.value) {
282+
update(event);
283+
}
284+
};
285+
this.eventEmitter.addListener('nftAssetEventUpdate', listener);
286+
return {
287+
unsubscribe: () => {
288+
this.eventEmitter.removeListener('nftAssetEventUpdate', listener);
289+
return this.rpcCall('unsubscribe', params);
290+
},
291+
};
292+
}
293+
294+
async subscribeNftCollectionEventUpdates(
295+
assetIdentifier: string,
296+
update: (event: NftEvent) => any
297+
): Promise<Subscription> {
298+
const params: RpcNftCollectionEventSubscriptionParams = {
299+
event: 'nft_collection_event',
300+
asset_identifier: assetIdentifier,
301+
};
302+
const subscribed = await this.rpcCall<{ asset_identifier: string }>('subscribe', params);
303+
const listener = (event: NftEvent) => {
304+
if (event.asset_identifier === subscribed.asset_identifier) {
305+
update(event);
306+
}
307+
};
308+
this.eventEmitter.addListener('nftCollectionEventUpdate', listener);
309+
return {
310+
unsubscribe: () => {
311+
this.eventEmitter.removeListener('nftCollectionEventUpdate', listener);
312+
return this.rpcCall('unsubscribe', params);
313+
},
314+
};
315+
}
235316
}
236317

237318
export async function connectWebSocketClient(

docs/entities/nft-events/nft-event.schema.json

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
"title": "NftEvent",
44
"additionalProperties": false,
55
"required": [
6-
"sender",
7-
"recipient",
86
"asset_identifier",
97
"value",
108
"tx_id",
11-
"block_height"
9+
"tx_index",
10+
"event_index",
11+
"block_height",
12+
"asset_event_type"
1213
],
1314
"properties": {
1415
"sender": {
@@ -20,6 +21,9 @@
2021
"asset_identifier": {
2122
"type": "string"
2223
},
24+
"asset_event_type": {
25+
"type": "string"
26+
},
2327
"value": {
2428
"type": "object",
2529
"required": ["hex", "repr"],
@@ -39,8 +43,14 @@
3943
"tx_id": {
4044
"type": "string"
4145
},
46+
"tx_index": {
47+
"type": "number"
48+
},
4249
"block_height": {
4350
"type": "number"
51+
},
52+
"event_index": {
53+
"type": "number"
4454
}
4555
}
4656
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"title": "RpcNftAssetEventSubscriptionParams",
3+
"description": "",
4+
"type": "object",
5+
"required": [
6+
"event",
7+
"asset_identifier",
8+
"value"
9+
],
10+
"additionalProperties": false,
11+
"properties": {
12+
"event": {
13+
"type": "string",
14+
"enum": ["nft_asset_event"]
15+
},
16+
"asset_identifier": {
17+
"type": "string"
18+
},
19+
"value": {
20+
"type": "string"
21+
}
22+
}
23+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"title": "RpcNftAssetEventSubscriptionRequest",
3+
"description": "",
4+
"type": "object",
5+
"required": ["jsonrpc", "id", "method", "params"],
6+
"additionalProperties": false,
7+
"properties": {
8+
"jsonrpc": {
9+
"type": "string",
10+
"enum": ["2.0"]
11+
},
12+
"id": {
13+
"type": ["number", "string"]
14+
},
15+
"method": {
16+
"type": "string",
17+
"enum": ["nft_asset_event"]
18+
},
19+
"params": {
20+
"$ref": "./rpc-nft-asset-event-subscription-params.schema.json"
21+
}
22+
}
23+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"title": "RpcNftCollectionEventSubscriptionParams",
3+
"description": "",
4+
"type": "object",
5+
"required": [
6+
"event",
7+
"asset_identifier"
8+
],
9+
"additionalProperties": false,
10+
"properties": {
11+
"event": {
12+
"type": "string",
13+
"enum": ["nft_collection_event"]
14+
},
15+
"asset_identifier": {
16+
"type": "string"
17+
}
18+
}
19+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"title": "RpcNftCollectionEventSubscriptionRequest",
3+
"description": "",
4+
"type": "object",
5+
"required": ["jsonrpc", "id", "method", "params"],
6+
"additionalProperties": false,
7+
"properties": {
8+
"jsonrpc": {
9+
"type": "string",
10+
"enum": ["2.0"]
11+
},
12+
"id": {
13+
"type": ["number", "string"]
14+
},
15+
"method": {
16+
"type": "string",
17+
"enum": ["nft_collection_event"]
18+
},
19+
"params": {
20+
"$ref": "./rpc-nft-collection-event-subscription-params.schema.json"
21+
}
22+
}
23+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"title": "RpcNftEventNotificationResponse",
3+
"description": "",
4+
"type": "object",
5+
"required": [
6+
"jsonrpc",
7+
"method",
8+
"params"
9+
],
10+
"additionalProperties": false,
11+
"properties": {
12+
"jsonrpc": {
13+
"type": "string",
14+
"enum": ["2.0"]
15+
},
16+
"method": {
17+
"type": "string",
18+
"enum": ["block"]
19+
},
20+
"params": {
21+
"$ref": "../nft-events/nft-event.schema.json"
22+
}
23+
}
24+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"title": "RpcNftEventSubscriptionParams",
3+
"description": "",
4+
"type": "object",
5+
"required": ["event"],
6+
"additionalProperties": false,
7+
"properties": {
8+
"event": {
9+
"type": "string",
10+
"enum": ["nft_event"]
11+
}
12+
}
13+
}

0 commit comments

Comments
 (0)