@novasamatech/host-papp 0.7.2 → 0.7.3
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 +207 -0
- package/dist/helpers/createAsyncTaskPool.d.ts +28 -0
- package/dist/helpers/createAsyncTaskPool.js +115 -0
- package/dist/helpers/createAsyncTaskPool.spec.d.ts +1 -0
- package/dist/helpers/createAsyncTaskPool.spec.js +138 -0
- package/dist/helpers/promiseWithResolvers.d.ts +6 -0
- package/dist/helpers/promiseWithResolvers.js +10 -0
- package/dist/helpers/utils.d.ts +8 -0
- package/dist/helpers/utils.js +10 -0
- package/dist/sso/sessionManager/userSession.js +80 -80
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -4,8 +4,215 @@ Polkadot app integration layer for host applications.
|
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
+
`@novasamatech/host-papp` is the integration SDK that lets a javascript-based host embed Polkadot
|
|
8
|
+
Mobile capabilities. It encapsulates everything needed to:
|
|
9
|
+
|
|
10
|
+
- pair the host with a Polkadot wallet/SSO provider via a deeplink handshake;
|
|
11
|
+
- store and manage paired user sessions;
|
|
12
|
+
- send signing requests and ring-VRF alias requests to the paired wallet;
|
|
13
|
+
- look up on-chain identity information for accounts.
|
|
14
|
+
|
|
15
|
+
The package is UI-framework agnostic — it exposes plain async APIs and observable state
|
|
16
|
+
(`subscribe` / `read`), so it can be wired into React, Vue, Svelte, vanilla DOM, or a
|
|
17
|
+
non-browser runtime.
|
|
18
|
+
|
|
7
19
|
## Installation
|
|
8
20
|
|
|
9
21
|
```shell
|
|
10
22
|
npm install @novasamatech/host-papp --save -E
|
|
11
23
|
```
|
|
24
|
+
|
|
25
|
+
## Getting started
|
|
26
|
+
|
|
27
|
+
Create a single adapter instance for the lifetime of your host app and share it across the
|
|
28
|
+
features that need it.
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { createPappAdapter } from '@novasamatech/host-papp';
|
|
32
|
+
|
|
33
|
+
const papp = createPappAdapter({
|
|
34
|
+
// Stable identifier for your host app — must not change between releases,
|
|
35
|
+
// otherwise existing pairings will be lost.
|
|
36
|
+
appId: 'my-host-app',
|
|
37
|
+
|
|
38
|
+
// URL to a JSON document describing the host: { name: string, icon: string }.
|
|
39
|
+
// The icon should be a rasterized image at least 256x256 px.
|
|
40
|
+
metadata: 'https://my-host-app.example/papp-metadata.json',
|
|
41
|
+
|
|
42
|
+
// Optional environment metadata shown on the wallet's confirmation screen.
|
|
43
|
+
hostMetadata: {
|
|
44
|
+
hostVersion: '1.4.0',
|
|
45
|
+
osType: 'macOS',
|
|
46
|
+
osVersion: '15.4',
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`createPappAdapter` returns four sub-modules:
|
|
52
|
+
|
|
53
|
+
| Module | Purpose |
|
|
54
|
+
| --------------- | ---------------------------------------------------------------------- |
|
|
55
|
+
| `papp.sso` | Authentication / pairing flow with a remote wallet. |
|
|
56
|
+
| `papp.sessions` | List of paired user sessions and per-session messaging (sign, etc.). |
|
|
57
|
+
| `papp.secrets` | Local secret storage for the derived guest accounts. |
|
|
58
|
+
| `papp.identity` | On-chain identity lookups for arbitrary account ids. |
|
|
59
|
+
|
|
60
|
+
Custom adapters (statement store, identity RPC, storage, lazy chain client) can be supplied
|
|
61
|
+
via the `adapters` option for testing or non-browser environments.
|
|
62
|
+
|
|
63
|
+
## Authentication and pairing
|
|
64
|
+
|
|
65
|
+
`papp.sso.authenticate()` runs the full pairing + attestation flow and resolves with the
|
|
66
|
+
stored user session, or `null` if the flow was aborted. The flow is idempotent — calling it
|
|
67
|
+
again while a previous run is in flight returns the same in-progress promise.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
const result = await papp.sso.authenticate();
|
|
71
|
+
|
|
72
|
+
result.match(
|
|
73
|
+
session => {
|
|
74
|
+
if (session) {
|
|
75
|
+
console.log('Paired with', session.remoteAccount.accountId);
|
|
76
|
+
} else {
|
|
77
|
+
console.log('Pairing aborted');
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
error => {
|
|
81
|
+
console.error('Pairing failed:', error);
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
To cancel a running flow:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
papp.sso.abortAuthentication();
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Reacting to pairing status
|
|
93
|
+
|
|
94
|
+
The pairing process is observable. UI code typically renders a QR code / deeplink while the
|
|
95
|
+
status is `pairing`, then transitions to a "signing in" screen during attestation.
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import type { PairingStatus } from '@novasamatech/host-papp';
|
|
99
|
+
|
|
100
|
+
const render = (status: PairingStatus) => {
|
|
101
|
+
switch (status.step) {
|
|
102
|
+
case 'none':
|
|
103
|
+
case 'initial':
|
|
104
|
+
return; // not started yet
|
|
105
|
+
case 'pairing':
|
|
106
|
+
// status.payload is a `polkadotapp://pair?handshake=…` deeplink —
|
|
107
|
+
// render it as a QR code or open it on mobile.
|
|
108
|
+
showDeeplink(status.payload);
|
|
109
|
+
return;
|
|
110
|
+
case 'pairingError':
|
|
111
|
+
showError(status.message);
|
|
112
|
+
return;
|
|
113
|
+
case 'finished':
|
|
114
|
+
showPairedAccount(status.session);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
render(papp.sso.pairingStatus.read());
|
|
120
|
+
const unsubscribe = papp.sso.pairingStatus.subscribe(render);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`papp.sso.attestationStatus` exposes the same `read` / `subscribe` shape and tracks
|
|
124
|
+
attestation progress (`attestation` with a claimed `username`, `attestationError`, or
|
|
125
|
+
`finished`). For convenience, treat the two streams as a single derived UI state — pairing
|
|
126
|
+
steps before `attestation`, then attestation, then back to pairing's `finished`.
|
|
127
|
+
|
|
128
|
+
## Managing user sessions
|
|
129
|
+
|
|
130
|
+
`papp.sessions.sessions` is an observable list of currently paired sessions. Most host apps
|
|
131
|
+
work with the first one (single-user model), but the SDK does not enforce that.
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import type { UserSession } from '@novasamatech/host-papp';
|
|
135
|
+
|
|
136
|
+
let currentSession: UserSession | null = null;
|
|
137
|
+
|
|
138
|
+
const unsubscribe = papp.sessions.sessions.subscribe(sessions => {
|
|
139
|
+
currentSession = sessions.at(0) ?? null;
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Initial value, in case a session was restored from storage on boot.
|
|
143
|
+
currentSession = papp.sessions.sessions.read().at(0) ?? null;
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Disconnecting notifies the wallet, removes local secrets, and triggers the subscription
|
|
147
|
+
above.
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
const disconnect = async (session: UserSession) => {
|
|
151
|
+
const result = await papp.sessions.disconnect(session);
|
|
152
|
+
result.match(
|
|
153
|
+
() => console.log('Disconnected'),
|
|
154
|
+
error => console.error('Disconnect failed:', error),
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Signing
|
|
160
|
+
|
|
161
|
+
A `UserSession` exposes `signPayload` and `signRaw` for forwarding signing requests to the
|
|
162
|
+
paired wallet.
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
const signed = await currentSession.signPayload({
|
|
166
|
+
address: '5G…', // SS58 address or 0x-prefixed account id
|
|
167
|
+
blockHash: '0x…',
|
|
168
|
+
blockNumber: '0x…',
|
|
169
|
+
era: '0x…',
|
|
170
|
+
genesisHash: '0x…',
|
|
171
|
+
method: '0x…',
|
|
172
|
+
nonce: '0x…',
|
|
173
|
+
specVersion: '0x…',
|
|
174
|
+
tip: '0x…',
|
|
175
|
+
transactionVersion: '0x…',
|
|
176
|
+
signedExtensions: ['CheckNonZeroSender', 'CheckSpecVersion' /* … */],
|
|
177
|
+
version: 4,
|
|
178
|
+
assetId: undefined,
|
|
179
|
+
metadataHash: undefined,
|
|
180
|
+
mode: undefined,
|
|
181
|
+
withSignedTransaction: undefined,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
signed.match(
|
|
185
|
+
response => submitSignedExtrinsic(response),
|
|
186
|
+
error => console.error('Signing rejected:', error),
|
|
187
|
+
);
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`signRaw` follows the same pattern but takes either raw `Bytes` or a `Payload` string:
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
await currentSession.signRaw({
|
|
194
|
+
address: '5G…',
|
|
195
|
+
data: { tag: 'Payload', value: 'Login challenge: abc123' },
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Identity lookups
|
|
200
|
+
|
|
201
|
+
`papp.identity` resolves on-chain identity data (lite / full username, credibility, slots)
|
|
202
|
+
for arbitrary account ids. Pass an `0x`-prefixed account id (32-byte hex).
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
const lookup = async (accountId: string) => {
|
|
206
|
+
const result = await papp.identity.getIdentity(accountId);
|
|
207
|
+
result.match(
|
|
208
|
+
identity => {
|
|
209
|
+
if (!identity) return;
|
|
210
|
+
console.log(identity.liteUsername, identity.credibility);
|
|
211
|
+
},
|
|
212
|
+
error => console.error('Identity lookup failed:', error),
|
|
213
|
+
);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// Batch lookup
|
|
217
|
+
await papp.identity.getIdentities([accountIdA, accountIdB]);
|
|
218
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ResultAsync } from 'neverthrow';
|
|
2
|
+
export declare const DEFAULT_POOL = "default";
|
|
3
|
+
type Params = {
|
|
4
|
+
poolSize: number;
|
|
5
|
+
retryCount: number;
|
|
6
|
+
retryDelay: ((attempt: number) => number) | number;
|
|
7
|
+
};
|
|
8
|
+
type TaskParams = {
|
|
9
|
+
pool?: string;
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Task manager with queues, retries and named pools.
|
|
14
|
+
*/
|
|
15
|
+
declare class AsyncTaskPool {
|
|
16
|
+
private config;
|
|
17
|
+
private events;
|
|
18
|
+
private queue;
|
|
19
|
+
private activeTasks;
|
|
20
|
+
constructor(config: Params);
|
|
21
|
+
call<T>(fn: () => ResultAsync<T, Error>, params?: TaskParams): ResultAsync<T, Error>;
|
|
22
|
+
settle(pool: string): Promise<void>;
|
|
23
|
+
private processPool;
|
|
24
|
+
private tryToSettlePool;
|
|
25
|
+
private retryDelay;
|
|
26
|
+
}
|
|
27
|
+
export declare const createAsyncTaskPool: (params: Params) => AsyncTaskPool;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { createNanoEvents } from 'nanoevents';
|
|
2
|
+
import { fromPromise } from 'neverthrow';
|
|
3
|
+
import { promiseWithResolvers } from './promiseWithResolvers.js';
|
|
4
|
+
import { nullable, toError } from './utils.js';
|
|
5
|
+
export const DEFAULT_POOL = 'default';
|
|
6
|
+
/**
|
|
7
|
+
* Task manager with queues, retries and named pools.
|
|
8
|
+
*/
|
|
9
|
+
class AsyncTaskPool {
|
|
10
|
+
config;
|
|
11
|
+
events = createNanoEvents();
|
|
12
|
+
queue = [];
|
|
13
|
+
activeTasks = [];
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
}
|
|
17
|
+
call(fn, params) {
|
|
18
|
+
const { resolve, reject, promise } = promiseWithResolvers();
|
|
19
|
+
const task = {
|
|
20
|
+
fn,
|
|
21
|
+
pool: params?.pool ?? DEFAULT_POOL,
|
|
22
|
+
retry: 0,
|
|
23
|
+
resolve,
|
|
24
|
+
reject,
|
|
25
|
+
signal: params?.signal,
|
|
26
|
+
};
|
|
27
|
+
this.queue.push(task);
|
|
28
|
+
this.processPool(task.pool);
|
|
29
|
+
return fromPromise(promise, toError);
|
|
30
|
+
}
|
|
31
|
+
settle(pool) {
|
|
32
|
+
if (this.queue.length === 0 && this.activeTasks.length === 0) {
|
|
33
|
+
return Promise.resolve();
|
|
34
|
+
}
|
|
35
|
+
return new Promise(resolve => {
|
|
36
|
+
const handler = (done) => {
|
|
37
|
+
if (done === pool) {
|
|
38
|
+
unsubscribe();
|
|
39
|
+
resolve();
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const unsubscribe = this.events.on('settled', handler);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
processPool(pool) {
|
|
46
|
+
let task = null;
|
|
47
|
+
const activeTasks = this.activeTasks.filter(x => x.pool === pool);
|
|
48
|
+
// skip this iteration since task pool at full capacity
|
|
49
|
+
if (activeTasks.length >= this.config.poolSize) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// finding the next task
|
|
53
|
+
for (const [index, potentialTask] of this.queue.entries()) {
|
|
54
|
+
if (potentialTask.pool !== pool) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
task = potentialTask;
|
|
58
|
+
this.queue.splice(index, 1);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
if (nullable(task)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.activeTasks.push(task);
|
|
65
|
+
const handleError = (task, error) => {
|
|
66
|
+
if (task.retry >= this.config.retryCount) {
|
|
67
|
+
task.reject(error);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
if (task.signal?.aborted) {
|
|
71
|
+
task.reject(error);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const retryDelay = this.retryDelay(task);
|
|
75
|
+
task.retry++;
|
|
76
|
+
setTimeout(() => {
|
|
77
|
+
this.queue.push(task);
|
|
78
|
+
this.processPool(pool);
|
|
79
|
+
}, retryDelay);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const finish = (task) => {
|
|
83
|
+
this.activeTasks = this.activeTasks.filter(x => x !== task);
|
|
84
|
+
this.tryToSettlePool(pool);
|
|
85
|
+
this.processPool(pool);
|
|
86
|
+
};
|
|
87
|
+
try {
|
|
88
|
+
task
|
|
89
|
+
.fn()
|
|
90
|
+
.andTee(result => {
|
|
91
|
+
task.resolve(result);
|
|
92
|
+
finish(task);
|
|
93
|
+
})
|
|
94
|
+
.orTee(error => {
|
|
95
|
+
handleError(task, error);
|
|
96
|
+
finish(task);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
handleError(task, toError(error));
|
|
101
|
+
finish(task);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
tryToSettlePool(pool) {
|
|
105
|
+
const activePoolTasks = this.activeTasks.find(x => x.pool === pool);
|
|
106
|
+
const queuedPoolTasks = this.queue.find(x => x.pool === pool);
|
|
107
|
+
if (nullable(activePoolTasks) && nullable(queuedPoolTasks)) {
|
|
108
|
+
this.events.emit('settled', pool);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
retryDelay(task) {
|
|
112
|
+
return typeof this.config.retryDelay === 'function' ? this.config.retryDelay(task.retry) : this.config.retryDelay;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export const createAsyncTaskPool = (params) => new AsyncTaskPool(params);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { setTimeout } from 'node:timers/promises';
|
|
2
|
+
import { err, fromPromise, ok, okAsync } from 'neverthrow';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { createAsyncTaskPool } from './createAsyncTaskPool.js';
|
|
5
|
+
import { toError } from './utils.js';
|
|
6
|
+
const delay = (ttl = 0) => setTimeout(ttl);
|
|
7
|
+
describe('asyncTaskPool', () => {
|
|
8
|
+
it('should exec async task', async () => {
|
|
9
|
+
const pool = createAsyncTaskPool({ poolSize: 1, retryCount: 0, retryDelay: () => 0 });
|
|
10
|
+
const result = await pool.call(() => fromPromise(delay().then(() => 'test'), toError));
|
|
11
|
+
expect(result).toEqual(ok('test'));
|
|
12
|
+
});
|
|
13
|
+
it('should handle sync errors', async () => {
|
|
14
|
+
const pool = createAsyncTaskPool({ poolSize: 1, retryCount: 0, retryDelay: () => 0 });
|
|
15
|
+
const error = new Error('test');
|
|
16
|
+
const result = await pool.call(() => {
|
|
17
|
+
throw error;
|
|
18
|
+
});
|
|
19
|
+
expect(result).toEqual(err(error));
|
|
20
|
+
});
|
|
21
|
+
it('should handle async errors', async () => {
|
|
22
|
+
const pool = createAsyncTaskPool({ poolSize: 1, retryCount: 0, retryDelay: () => 0 });
|
|
23
|
+
const error = new Error('test');
|
|
24
|
+
const result = await pool.call(() => fromPromise(Promise.reject(error), toError));
|
|
25
|
+
return expect(result).toEqual(err(error));
|
|
26
|
+
});
|
|
27
|
+
it('should handle queue', async () => {
|
|
28
|
+
const pool = createAsyncTaskPool({ poolSize: 2, retryCount: 0, retryDelay: () => 0 });
|
|
29
|
+
const spy = vi.fn();
|
|
30
|
+
await Promise.all([pool.call(spy), pool.call(spy), pool.call(spy), pool.call(spy)]);
|
|
31
|
+
expect(spy).toBeCalledTimes(4);
|
|
32
|
+
});
|
|
33
|
+
it('should update pool in correct order', async () => {
|
|
34
|
+
const pool = createAsyncTaskPool({ poolSize: 2, retryCount: 0, retryDelay: () => 0 });
|
|
35
|
+
const result = [];
|
|
36
|
+
const res = Promise.all([
|
|
37
|
+
pool.call(() => fromPromise(delay(800).then(() => result.push(1)), toError)),
|
|
38
|
+
pool.call(() => fromPromise(delay(100).then(() => result.push(2)), toError)),
|
|
39
|
+
pool.call(() => fromPromise(delay(500).then(() => result.push(3)), toError)),
|
|
40
|
+
pool.call(() => fromPromise(delay(100).then(() => result.push(4)), toError)),
|
|
41
|
+
]);
|
|
42
|
+
await res;
|
|
43
|
+
expect(result).toEqual([2, 3, 4, 1]);
|
|
44
|
+
});
|
|
45
|
+
it('should retry', async () => {
|
|
46
|
+
const pool = createAsyncTaskPool({ poolSize: 1, retryCount: 1, retryDelay: () => 0 });
|
|
47
|
+
let tries = 0;
|
|
48
|
+
const result = await pool.call(() => {
|
|
49
|
+
if (tries === 1) {
|
|
50
|
+
return okAsync('test');
|
|
51
|
+
}
|
|
52
|
+
tries++;
|
|
53
|
+
throw new Error();
|
|
54
|
+
});
|
|
55
|
+
expect(result).toEqual(ok('test'));
|
|
56
|
+
});
|
|
57
|
+
it('should throw on retry limit exceeding', async () => {
|
|
58
|
+
const spy = vi.fn(() => 0);
|
|
59
|
+
const pool = createAsyncTaskPool({ poolSize: 1, retryCount: 1, retryDelay: spy });
|
|
60
|
+
let tries = 0;
|
|
61
|
+
const result = await pool.call(() => {
|
|
62
|
+
if (tries === 2) {
|
|
63
|
+
return okAsync('test');
|
|
64
|
+
}
|
|
65
|
+
tries++;
|
|
66
|
+
throw new Error();
|
|
67
|
+
});
|
|
68
|
+
expect(spy).toBeCalledTimes(1);
|
|
69
|
+
expect(result).toEqual(err(new Error()));
|
|
70
|
+
});
|
|
71
|
+
it('should correctly calculate retry delay', async () => {
|
|
72
|
+
const spy = vi.fn((retry) => retry * 10);
|
|
73
|
+
const pool = createAsyncTaskPool({ poolSize: 1, retryCount: 2, retryDelay: spy });
|
|
74
|
+
let tries = 0;
|
|
75
|
+
await pool.call(() => {
|
|
76
|
+
if (tries === 2) {
|
|
77
|
+
return okAsync('test');
|
|
78
|
+
}
|
|
79
|
+
tries++;
|
|
80
|
+
throw new Error();
|
|
81
|
+
});
|
|
82
|
+
expect(spy).toHaveBeenCalledTimes(2);
|
|
83
|
+
expect(spy.mock.calls).toEqual([[0], [1]]);
|
|
84
|
+
});
|
|
85
|
+
it('should create multiple pools', async () => {
|
|
86
|
+
const spy = vi.fn();
|
|
87
|
+
const pool = createAsyncTaskPool({ poolSize: 1, retryCount: 0, retryDelay: 0 });
|
|
88
|
+
const tasks = [
|
|
89
|
+
{ delay: 600, value: 1, pool: '1' },
|
|
90
|
+
{ delay: 400, value: 2, pool: '2' },
|
|
91
|
+
{ delay: 100, value: 3, pool: '1' },
|
|
92
|
+
{ delay: 0, value: 4, pool: '2' },
|
|
93
|
+
];
|
|
94
|
+
const result = [];
|
|
95
|
+
for (const task of tasks) {
|
|
96
|
+
const call = pool.call(() => fromPromise(delay(task.delay).then(() => spy(task.value)), toError), { pool: task.pool });
|
|
97
|
+
result.push(call);
|
|
98
|
+
}
|
|
99
|
+
await Promise.all(result);
|
|
100
|
+
expect(spy).toHaveBeenCalledTimes(4);
|
|
101
|
+
expect(spy.mock.calls).toEqual([[2], [4], [1], [3]]);
|
|
102
|
+
}, 10000);
|
|
103
|
+
it('should settle all tasks', async () => {
|
|
104
|
+
const pool = createAsyncTaskPool({ poolSize: 1, retryCount: 0, retryDelay: 0 });
|
|
105
|
+
const tasks = [
|
|
106
|
+
{ delay: 0, value: 1 },
|
|
107
|
+
{ delay: 0, value: 2 },
|
|
108
|
+
{ delay: 0, value: 3 },
|
|
109
|
+
{ delay: 0, value: 4 },
|
|
110
|
+
];
|
|
111
|
+
const result = [];
|
|
112
|
+
for (const task of tasks) {
|
|
113
|
+
pool.call(() => fromPromise(delay(task.delay), toError).andTee(() => result.push(task.value)), { pool: 'test' });
|
|
114
|
+
}
|
|
115
|
+
await pool.settle('test');
|
|
116
|
+
expect(result).toEqual([1, 2, 3, 4]);
|
|
117
|
+
});
|
|
118
|
+
it('should settle tasks that was created by chain reaction', async () => {
|
|
119
|
+
const pool = createAsyncTaskPool({ poolSize: 1, retryCount: 0, retryDelay: 0 });
|
|
120
|
+
const tasks = [
|
|
121
|
+
{ delay: 10, value: 1 },
|
|
122
|
+
{ delay: 10, value: 2 },
|
|
123
|
+
{ delay: 10, value: 3 },
|
|
124
|
+
{ delay: 10, value: 4 },
|
|
125
|
+
];
|
|
126
|
+
const result = [];
|
|
127
|
+
for (const task of tasks) {
|
|
128
|
+
pool
|
|
129
|
+
.call(() => fromPromise(delay(task.delay), toError), { pool: 'test' })
|
|
130
|
+
.then(() => {
|
|
131
|
+
result.push(task.value);
|
|
132
|
+
pool.call(() => fromPromise(delay(task.delay).then(() => result.push(task.value + 10)), toError), { pool: 'test' });
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
await pool.settle('test');
|
|
136
|
+
expect(result).toEqual([1, 2, 3, 4, 11, 12, 13, 14]);
|
|
137
|
+
});
|
|
138
|
+
});
|
package/dist/helpers/utils.d.ts
CHANGED
package/dist/helpers/utils.js
CHANGED
|
@@ -7,3 +7,13 @@ export function toError(err) {
|
|
|
7
7
|
}
|
|
8
8
|
return new Error('Unknown error occurred.');
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Type guard that checks is value nullable
|
|
12
|
+
*
|
|
13
|
+
* @param value Value to be checked
|
|
14
|
+
*
|
|
15
|
+
* @returns {Boolean}
|
|
16
|
+
*/
|
|
17
|
+
export function nullable(value) {
|
|
18
|
+
return value === null || value === undefined;
|
|
19
|
+
}
|
|
@@ -3,11 +3,13 @@ import { enumValue, toHex } from '@novasamatech/scale';
|
|
|
3
3
|
import { createSession } from '@novasamatech/statement-store';
|
|
4
4
|
import { fieldListView } from '@novasamatech/storage-adapter';
|
|
5
5
|
import { nanoid } from 'nanoid';
|
|
6
|
-
import { ResultAsync, err,
|
|
6
|
+
import { ResultAsync, err, ok, okAsync } from 'neverthrow';
|
|
7
7
|
import { AccountId } from 'polkadot-api';
|
|
8
|
+
import { createAsyncTaskPool } from '../../helpers/createAsyncTaskPool.js';
|
|
8
9
|
import { RemoteMessageCodec } from './scale/remoteMessage.js';
|
|
9
10
|
export function createUserSession({ userSession, statementStore, encryption, storage, prover, }) {
|
|
10
11
|
const accountId = AccountId();
|
|
12
|
+
const requestQueue = createAsyncTaskPool({ poolSize: 1, retryCount: 0, retryDelay: 0 });
|
|
11
13
|
const session = createSession({
|
|
12
14
|
localAccount: userSession.localAccount,
|
|
13
15
|
remoteAccount: userSession.remoteAccount,
|
|
@@ -22,7 +24,7 @@ export function createUserSession({ userSession, statementStore, encryption, sto
|
|
|
22
24
|
to: JSON.stringify,
|
|
23
25
|
});
|
|
24
26
|
function toAccountId(address) {
|
|
25
|
-
// already account id
|
|
27
|
+
// already an account id
|
|
26
28
|
if (address.startsWith('0x') && address.length === 64 + 2) {
|
|
27
29
|
return address;
|
|
28
30
|
}
|
|
@@ -36,74 +38,92 @@ export function createUserSession({ userSession, statementStore, encryption, sto
|
|
|
36
38
|
localAccount: userSession.localAccount,
|
|
37
39
|
remoteAccount: userSession.remoteAccount,
|
|
38
40
|
signPayload(payload) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return err(new Error(message.value));
|
|
66
|
-
}
|
|
41
|
+
return requestQueue.call(() => {
|
|
42
|
+
const messageId = nanoid();
|
|
43
|
+
const request = session.request(RemoteMessageCodec, {
|
|
44
|
+
messageId,
|
|
45
|
+
data: enumValue('v1', enumValue('SignRequest', enumValue('Payload', {
|
|
46
|
+
...payload,
|
|
47
|
+
address: toAddress(toAccountId(payload.address)),
|
|
48
|
+
}))),
|
|
49
|
+
});
|
|
50
|
+
const responseFilter = (message) => {
|
|
51
|
+
if (message.data.tag === 'v1' &&
|
|
52
|
+
message.data.value.tag === 'SignResponse' &&
|
|
53
|
+
message.data.value.value.respondingTo === messageId) {
|
|
54
|
+
return message.data.value.value.payload;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
return request
|
|
58
|
+
.andThen(() => session.waitForRequestMessage(RemoteMessageCodec, responseFilter))
|
|
59
|
+
.andThen(message => {
|
|
60
|
+
if (message.success) {
|
|
61
|
+
return ok(message.value);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
return err(new Error(message.value));
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
67
|
});
|
|
68
68
|
},
|
|
69
69
|
signRaw(payload) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return err(new Error(message.value));
|
|
97
|
-
}
|
|
70
|
+
return requestQueue.call(() => {
|
|
71
|
+
const messageId = nanoid();
|
|
72
|
+
const request = session.request(RemoteMessageCodec, {
|
|
73
|
+
messageId,
|
|
74
|
+
data: enumValue('v1', enumValue('SignRequest', enumValue('Raw', {
|
|
75
|
+
...payload,
|
|
76
|
+
address: toAddress(toAccountId(payload.address)),
|
|
77
|
+
}))),
|
|
78
|
+
});
|
|
79
|
+
const responseFilter = (message) => {
|
|
80
|
+
if (message.data.tag === 'v1' &&
|
|
81
|
+
message.data.value.tag === 'SignResponse' &&
|
|
82
|
+
message.data.value.value.respondingTo === messageId) {
|
|
83
|
+
return message.data.value.value.payload;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
return request
|
|
87
|
+
.andThen(() => session.waitForRequestMessage(RemoteMessageCodec, responseFilter))
|
|
88
|
+
.andThen(message => {
|
|
89
|
+
if (message.success) {
|
|
90
|
+
return ok(message.value);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
return err(new Error(message.value));
|
|
94
|
+
}
|
|
95
|
+
});
|
|
98
96
|
});
|
|
99
97
|
},
|
|
100
98
|
sendDisconnectMessage() {
|
|
101
|
-
return session
|
|
99
|
+
return requestQueue.call(() => session
|
|
102
100
|
.submitRequestMessage(RemoteMessageCodec, {
|
|
103
101
|
messageId: nanoid(),
|
|
104
102
|
data: enumValue('v1', enumValue('Disconnected', undefined)),
|
|
105
103
|
})
|
|
106
|
-
.map(() => undefined);
|
|
104
|
+
.map(() => undefined));
|
|
105
|
+
},
|
|
106
|
+
getRingVrfAlias(productAccountId, productId) {
|
|
107
|
+
return requestQueue.call(() => {
|
|
108
|
+
const messageId = nanoid();
|
|
109
|
+
const request = session.request(RemoteMessageCodec, {
|
|
110
|
+
messageId,
|
|
111
|
+
data: enumValue('v1', enumValue('RingVrfAliasRequest', {
|
|
112
|
+
productAccountId,
|
|
113
|
+
productId,
|
|
114
|
+
})),
|
|
115
|
+
});
|
|
116
|
+
const responseFilter = (message) => {
|
|
117
|
+
if (message.data.tag === 'v1' &&
|
|
118
|
+
message.data.value.tag === 'RingVrfAliasResponse' &&
|
|
119
|
+
message.data.value.value.respondingTo === messageId) {
|
|
120
|
+
return message.data.value.value.payload;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
return request
|
|
124
|
+
.andThen(() => session.waitForRequestMessage(RemoteMessageCodec, responseFilter))
|
|
125
|
+
.andThen(result => (result.success ? ok(result.value) : err(new Error(result.value))));
|
|
126
|
+
});
|
|
107
127
|
},
|
|
108
128
|
subscribe(callback) {
|
|
109
129
|
return session.subscribe(RemoteMessageCodec, messages => {
|
|
@@ -134,26 +154,6 @@ export function createUserSession({ userSession, statementStore, encryption, sto
|
|
|
134
154
|
});
|
|
135
155
|
});
|
|
136
156
|
},
|
|
137
|
-
getRingVrfAlias(productAccountId, productId) {
|
|
138
|
-
const messageId = nanoid();
|
|
139
|
-
const request = session.request(RemoteMessageCodec, {
|
|
140
|
-
messageId,
|
|
141
|
-
data: enumValue('v1', enumValue('RingVrfAliasRequest', {
|
|
142
|
-
productAccountId,
|
|
143
|
-
productId,
|
|
144
|
-
})),
|
|
145
|
-
});
|
|
146
|
-
const responseFilter = (message) => {
|
|
147
|
-
if (message.data.tag === 'v1' &&
|
|
148
|
-
message.data.value.tag === 'RingVrfAliasResponse' &&
|
|
149
|
-
message.data.value.value.respondingTo === messageId) {
|
|
150
|
-
return message.data.value.value.payload;
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
return request
|
|
154
|
-
.andThen(() => session.waitForRequestMessage(RemoteMessageCodec, responseFilter))
|
|
155
|
-
.andThen(result => (result.success ? ok(result.value) : err(new Error(result.value))));
|
|
156
|
-
},
|
|
157
157
|
dispose() {
|
|
158
158
|
return session.dispose();
|
|
159
159
|
},
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@novasamatech/host-papp",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.3",
|
|
5
5
|
"description": "Polkadot app integration",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
@@ -29,11 +29,11 @@
|
|
|
29
29
|
"@noble/ciphers": "2.2.0",
|
|
30
30
|
"@noble/curves": "2.2.0",
|
|
31
31
|
"@noble/hashes": "2.2.0",
|
|
32
|
-
"@novasamatech/host-api": "0.7.
|
|
33
|
-
"@novasamatech/scale": "0.7.
|
|
34
|
-
"@novasamatech/statement-store": "0.7.
|
|
35
|
-
"@novasamatech/storage-adapter": "0.7.
|
|
36
|
-
"@polkadot-labs/hdkd-helpers": "^0.0.
|
|
32
|
+
"@novasamatech/host-api": "0.7.3",
|
|
33
|
+
"@novasamatech/scale": "0.7.3",
|
|
34
|
+
"@novasamatech/statement-store": "0.7.3",
|
|
35
|
+
"@novasamatech/storage-adapter": "0.7.3",
|
|
36
|
+
"@polkadot-labs/hdkd-helpers": "^0.0.30",
|
|
37
37
|
"nanoevents": "9.1.0",
|
|
38
38
|
"nanoid": "5.1.9",
|
|
39
39
|
"neverthrow": "^8.2.0",
|