@totemsdk/chain-provider 0.1.2 → 0.1.4
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/LICENSE +21 -0
- package/README.md +66 -24
- package/dist/index.d.ts +2 -1
- package/dist/index.js +11 -4
- package/dist/providers/composite.js +5 -1
- package/dist/providers/hosted.d.ts +35 -2
- package/dist/providers/hosted.js +167 -47
- package/dist/providers/lookup-client.d.ts +62 -0
- package/dist/providers/lookup-client.js +67 -0
- package/dist/providers/lookup.js +7 -2
- package/dist/providers/pureminima.js +5 -1
- package/dist/types.js +2 -1
- package/package.json +32 -6
- package/src/__tests__/chain-provider.test.ts +0 -116
- package/src/index.ts +0 -19
- package/src/providers/composite.ts +0 -72
- package/src/providers/hosted.ts +0 -129
- package/src/providers/lookup.ts +0 -55
- package/src/providers/pureminima.ts +0 -95
- package/src/types.ts +0 -73
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Totem SDK Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -16,13 +16,14 @@ npm install @totemsdk/chain-provider
|
|
|
16
16
|
|
|
17
17
|
```typescript
|
|
18
18
|
interface ChainStateProvider {
|
|
19
|
-
getCoins(query:
|
|
20
|
-
getCoin(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
getToken(
|
|
24
|
-
searchTokens(query:
|
|
25
|
-
|
|
19
|
+
getCoins(query: CoinsQuery): Promise<Coin[]>;
|
|
20
|
+
getCoin(coinId: string): Promise<Coin | null>;
|
|
21
|
+
getProof(coinId: string): Promise<MMRProof>;
|
|
22
|
+
getTip(): Promise<ChainTip>;
|
|
23
|
+
getToken(tokenId: string): Promise<TokenInfo>;
|
|
24
|
+
searchTokens(query: TokenSearchQuery): Promise<TokenInfo[]>;
|
|
25
|
+
getTokensByCreator(address: string): Promise<TokenInfo[]>;
|
|
26
|
+
broadcastTxPoW(txpowHex: string): Promise<BroadcastResult>;
|
|
26
27
|
}
|
|
27
28
|
```
|
|
28
29
|
|
|
@@ -32,7 +33,7 @@ interface ChainStateProvider {
|
|
|
32
33
|
|-------|------------|
|
|
33
34
|
| `HostedProvider` | Axia / MEG hosted API (requires project credentials) |
|
|
34
35
|
| `PureMinimaRpcProvider` | A local or self-hosted PureMinima node directly |
|
|
35
|
-
| `
|
|
36
|
+
| `LookupClientProvider` | A personal lookup node over Hyperswarm DHT (via `@totemsdk/lookup-client`) |
|
|
36
37
|
| `CompositeProvider` | Fans out across multiple providers with fallback logic |
|
|
37
38
|
|
|
38
39
|
Five other packages (`omnia`, `statechain`, `lookup-node`, `lookup-client`, `chain-provider` itself) accept `ChainStateProvider` — this is the pivot point between the upper SDK and chain data.
|
|
@@ -50,7 +51,7 @@ const provider = new HostedProvider({
|
|
|
50
51
|
projectSecret: process.env.AXIA_SECRET,
|
|
51
52
|
});
|
|
52
53
|
|
|
53
|
-
const tip
|
|
54
|
+
const tip = await provider.getTip();
|
|
54
55
|
const coins = await provider.getCoins({ address: 'Mx...' });
|
|
55
56
|
```
|
|
56
57
|
|
|
@@ -64,31 +65,72 @@ const provider = new PureMinimaRpcProvider({
|
|
|
64
65
|
});
|
|
65
66
|
```
|
|
66
67
|
|
|
68
|
+
### Personal lookup node (sovereign, zero-Axia-dependency)
|
|
69
|
+
|
|
70
|
+
Connect directly to your own lookup node over Hyperswarm DHT. No hosted infrastructure required.
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { LookupClientProvider } from '@totemsdk/chain-provider';
|
|
74
|
+
import { connectLookupNode } from '@totemsdk/lookup-client';
|
|
75
|
+
|
|
76
|
+
// Connect to your personal lookup node
|
|
77
|
+
const client = await connectLookupNode({ hyperswarmTopic: 'deadbeef...' });
|
|
78
|
+
|
|
79
|
+
const provider = new LookupClientProvider(client);
|
|
80
|
+
|
|
81
|
+
const tip = await provider.getTip();
|
|
82
|
+
const coins = await provider.getCoins({ address: 'Mx...' });
|
|
83
|
+
|
|
84
|
+
// Clean up when done
|
|
85
|
+
client.disconnect();
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`@totemsdk/lookup-client` is a peer dependency — install it separately:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npm install @totemsdk/lookup-client
|
|
92
|
+
```
|
|
93
|
+
|
|
67
94
|
### Composite with fallback
|
|
68
95
|
|
|
96
|
+
Fan out across providers: try sovereign first, fall back to hosted on any error.
|
|
97
|
+
|
|
69
98
|
```typescript
|
|
70
|
-
import { CompositeProvider, HostedProvider,
|
|
99
|
+
import { CompositeProvider, HostedProvider, LookupClientProvider } from '@totemsdk/chain-provider';
|
|
100
|
+
import { connectLookupNode } from '@totemsdk/lookup-client';
|
|
71
101
|
|
|
72
|
-
const
|
|
73
|
-
new
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
102
|
+
const client = await connectLookupNode({ hyperswarmTopic: 'deadbeef...' });
|
|
103
|
+
const hosted = new HostedProvider({ baseUrl: 'https://api.axia.to', projectId: '...' });
|
|
104
|
+
|
|
105
|
+
const provider = new CompositeProvider(
|
|
106
|
+
new LookupClientProvider(client), // primary — sovereign, no Axia dependency
|
|
107
|
+
hosted, // fallback — hosted, always available
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Queries hit the lookup node first; fall back to hosted on error
|
|
111
|
+
const tip = await provider.getTip();
|
|
77
112
|
```
|
|
78
113
|
|
|
79
|
-
|
|
114
|
+
## LookupClientLike interface
|
|
115
|
+
|
|
116
|
+
`LookupClientProvider` accepts any object that satisfies the `LookupClientLike` structural interface exported from this package. This allows testing with a mock without importing `@totemsdk/lookup-client`:
|
|
80
117
|
|
|
81
118
|
```typescript
|
|
82
|
-
import {
|
|
119
|
+
import { LookupClientProvider } from '@totemsdk/chain-provider';
|
|
120
|
+
import type { LookupClientLike } from '@totemsdk/chain-provider';
|
|
83
121
|
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
122
|
+
const mockClient: LookupClientLike = {
|
|
123
|
+
getTip: async () => ({ block: 100, hash: '0x...' }),
|
|
124
|
+
getCoins: async () => [],
|
|
125
|
+
// ... etc
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const provider = new LookupClientProvider(mockClient);
|
|
87
129
|
```
|
|
88
130
|
|
|
89
131
|
## See also
|
|
90
132
|
|
|
91
|
-
- [`@totemsdk/pureminima-rpc`](
|
|
92
|
-
- [`@totemsdk/lookup-client`](
|
|
93
|
-
- [`@totemsdk/node`](
|
|
94
|
-
- [`@totemsdk/omnia`](
|
|
133
|
+
- [`@totemsdk/pureminima-rpc`](https://www.npmjs.com/package/@totemsdk/pureminima-rpc) — the RPC client used by `PureMinimaRpcProvider`
|
|
134
|
+
- [`@totemsdk/lookup-client`](https://www.npmjs.com/package/@totemsdk/lookup-client) — connect to a personal lookup node over Hyperswarm DHT
|
|
135
|
+
- [`@totemsdk/node`](https://www.npmjs.com/package/@totemsdk/node) — Node.js `MinimaProvider`
|
|
136
|
+
- [`@totemsdk/omnia`](https://www.npmjs.com/package/@totemsdk/omnia) — uses `ChainStateProvider` for coin queries
|
package/dist/index.d.ts
CHANGED
|
@@ -2,5 +2,6 @@ export type { ChainStateProvider, CoinsQuery, Coin, MMRProof, ChainTip, TokenInf
|
|
|
2
2
|
export { HostedProvider } from './providers/hosted.js';
|
|
3
3
|
export type { HostedProviderConfig } from './providers/hosted.js';
|
|
4
4
|
export { PureMinimaRpcProvider } from './providers/pureminima.js';
|
|
5
|
-
export {
|
|
5
|
+
export { LookupClientProvider } from './providers/lookup-client.js';
|
|
6
|
+
export type { LookupClientLike } from './providers/lookup-client.js';
|
|
6
7
|
export { CompositeProvider } from './providers/composite.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CompositeProvider = exports.LookupClientProvider = exports.PureMinimaRpcProvider = exports.HostedProvider = void 0;
|
|
4
|
+
var hosted_js_1 = require("./providers/hosted.js");
|
|
5
|
+
Object.defineProperty(exports, "HostedProvider", { enumerable: true, get: function () { return hosted_js_1.HostedProvider; } });
|
|
6
|
+
var pureminima_js_1 = require("./providers/pureminima.js");
|
|
7
|
+
Object.defineProperty(exports, "PureMinimaRpcProvider", { enumerable: true, get: function () { return pureminima_js_1.PureMinimaRpcProvider; } });
|
|
8
|
+
var lookup_client_js_1 = require("./providers/lookup-client.js");
|
|
9
|
+
Object.defineProperty(exports, "LookupClientProvider", { enumerable: true, get: function () { return lookup_client_js_1.LookupClientProvider; } });
|
|
10
|
+
var composite_js_1 = require("./providers/composite.js");
|
|
11
|
+
Object.defineProperty(exports, "CompositeProvider", { enumerable: true, get: function () { return composite_js_1.CompositeProvider; } });
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/**
|
|
2
3
|
* CompositeProvider — tries primary provider, falls back to secondary on any error.
|
|
3
4
|
*/
|
|
4
|
-
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.CompositeProvider = void 0;
|
|
7
|
+
class CompositeProvider {
|
|
5
8
|
constructor(primary, fallback, onFallback) {
|
|
6
9
|
this.primary = primary;
|
|
7
10
|
this.fallback = fallback;
|
|
@@ -48,3 +51,4 @@ export class CompositeProvider {
|
|
|
48
51
|
return this.withArg('broadcastTxPoW', txpowHex);
|
|
49
52
|
}
|
|
50
53
|
}
|
|
54
|
+
exports.CompositeProvider = CompositeProvider;
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* HostedProvider — wraps
|
|
3
|
-
*
|
|
2
|
+
* HostedProvider — wraps Axia REST + RPC API endpoints.
|
|
3
|
+
*
|
|
4
|
+
* Endpoint map (all require x-api-key header):
|
|
5
|
+
* GET /v1/wallet/utxos/:address — coin list (totem-shared key)
|
|
6
|
+
* POST /v1/wallet/rpc — Minima RPC commands (totem-shared key)
|
|
7
|
+
* POST /api/meg/postminedtxn — broadcast mined TxPoW (no auth required)
|
|
8
|
+
*
|
|
9
|
+
* For dApp developers using custom project API keys the `/v1/wallet/*` routes
|
|
10
|
+
* require x-api-key: 'totem-shared'. Use the apiKey config option accordingly.
|
|
4
11
|
*/
|
|
5
12
|
import type { ChainStateProvider, CoinsQuery, Coin, MMRProof, ChainTip, TokenInfo, TokenSearchQuery, BroadcastResult } from '../types.js';
|
|
6
13
|
export interface HostedProviderConfig {
|
|
@@ -14,12 +21,38 @@ export declare class HostedProvider implements ChainStateProvider {
|
|
|
14
21
|
private readonly timeoutMs;
|
|
15
22
|
constructor(config: HostedProviderConfig);
|
|
16
23
|
private fetchJson;
|
|
24
|
+
/**
|
|
25
|
+
* Send a Minima command to POST /v1/wallet/rpc.
|
|
26
|
+
* Returns the `response` field of the Minima envelope.
|
|
27
|
+
*/
|
|
28
|
+
private rpc;
|
|
29
|
+
/**
|
|
30
|
+
* Get coins (UTXOs) for an address.
|
|
31
|
+
* Maps GET /v1/wallet/utxos/:address → Coin[].
|
|
32
|
+
*/
|
|
17
33
|
getCoins(query: CoinsQuery): Promise<Coin[]>;
|
|
18
34
|
getCoin(coinId: string): Promise<Coin | null>;
|
|
35
|
+
/**
|
|
36
|
+
* Get MMR proof for a coin via Minima `coinproof` RPC command.
|
|
37
|
+
*/
|
|
19
38
|
getProof(coinId: string): Promise<MMRProof>;
|
|
39
|
+
/**
|
|
40
|
+
* Get chain tip via Minima `status` RPC — parses response.chain.
|
|
41
|
+
*/
|
|
20
42
|
getTip(): Promise<ChainTip>;
|
|
43
|
+
/**
|
|
44
|
+
* Get token info via Minima `tokens tokenid:X` RPC command.
|
|
45
|
+
*/
|
|
21
46
|
getToken(tokenId: string): Promise<TokenInfo>;
|
|
47
|
+
/**
|
|
48
|
+
* Search tokens via Minima `tokens` RPC command, then filter client-side.
|
|
49
|
+
*/
|
|
22
50
|
searchTokens(query: TokenSearchQuery): Promise<TokenInfo[]>;
|
|
23
51
|
getTokensByCreator(address: string): Promise<TokenInfo[]>;
|
|
52
|
+
/**
|
|
53
|
+
* Broadcast a mined TxPoW hex.
|
|
54
|
+
* Uses POST /api/meg/postminedtxn (the correct Axia MEG broadcast bridge).
|
|
55
|
+
*/
|
|
24
56
|
broadcastTxPoW(txpowHex: string): Promise<BroadcastResult>;
|
|
57
|
+
private _mapToken;
|
|
25
58
|
}
|
package/dist/providers/hosted.js
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/**
|
|
2
|
-
* HostedProvider — wraps
|
|
3
|
-
*
|
|
3
|
+
* HostedProvider — wraps Axia REST + RPC API endpoints.
|
|
4
|
+
*
|
|
5
|
+
* Endpoint map (all require x-api-key header):
|
|
6
|
+
* GET /v1/wallet/utxos/:address — coin list (totem-shared key)
|
|
7
|
+
* POST /v1/wallet/rpc — Minima RPC commands (totem-shared key)
|
|
8
|
+
* POST /api/meg/postminedtxn — broadcast mined TxPoW (no auth required)
|
|
9
|
+
*
|
|
10
|
+
* For dApp developers using custom project API keys the `/v1/wallet/*` routes
|
|
11
|
+
* require x-api-key: 'totem-shared'. Use the apiKey config option accordingly.
|
|
4
12
|
*/
|
|
5
|
-
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.HostedProvider = void 0;
|
|
15
|
+
class HostedProvider {
|
|
6
16
|
constructor(config) {
|
|
7
17
|
this.baseUrl = config.baseUrl.replace(/\/$/, '');
|
|
8
18
|
this.apiKey = config.apiKey;
|
|
@@ -30,75 +40,185 @@ export class HostedProvider {
|
|
|
30
40
|
clearTimeout(timer);
|
|
31
41
|
}
|
|
32
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Send a Minima command to POST /v1/wallet/rpc.
|
|
45
|
+
* Returns the `response` field of the Minima envelope.
|
|
46
|
+
*/
|
|
47
|
+
async rpc(command) {
|
|
48
|
+
const controller = new AbortController();
|
|
49
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
50
|
+
try {
|
|
51
|
+
const res = await fetch(`${this.baseUrl}/v1/wallet/rpc`, {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
signal: controller.signal,
|
|
54
|
+
headers: {
|
|
55
|
+
'Content-Type': 'text/plain',
|
|
56
|
+
'x-api-key': this.apiKey,
|
|
57
|
+
},
|
|
58
|
+
body: command,
|
|
59
|
+
});
|
|
60
|
+
if (!res.ok)
|
|
61
|
+
throw new Error(`HostedProvider RPC "${command}" HTTP ${res.status}`);
|
|
62
|
+
const json = await res.json();
|
|
63
|
+
if (json.status === false)
|
|
64
|
+
throw new Error(`RPC "${command}" failed: ${json.error ?? 'unknown'}`);
|
|
65
|
+
return json.response;
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get coins (UTXOs) for an address.
|
|
73
|
+
* Maps GET /v1/wallet/utxos/:address → Coin[].
|
|
74
|
+
*/
|
|
33
75
|
async getCoins(query) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
76
|
+
if (!query.address && !query.coinId)
|
|
77
|
+
return [];
|
|
78
|
+
if (query.coinId) {
|
|
79
|
+
const coin = await this.getCoin(query.coinId);
|
|
80
|
+
return coin ? [coin] : [];
|
|
81
|
+
}
|
|
82
|
+
const data = await this.fetchJson(`/v1/wallet/utxos/${encodeURIComponent(query.address)}`);
|
|
83
|
+
const raw = data.utxos ?? [];
|
|
84
|
+
return raw
|
|
85
|
+
.filter((u) => {
|
|
86
|
+
if (query.tokenId && u.tokenid !== query.tokenId)
|
|
87
|
+
return false;
|
|
88
|
+
if (query.sendable !== undefined && !!u.spent !== !query.sendable)
|
|
89
|
+
return false;
|
|
90
|
+
return true;
|
|
91
|
+
})
|
|
92
|
+
.map((u) => ({
|
|
93
|
+
coinid: u.coinid ?? u.id ?? '',
|
|
94
|
+
amount: String(u.amount ?? '0'),
|
|
95
|
+
address: u.address ?? query.address ?? '',
|
|
96
|
+
miniaddress: u.miniaddress,
|
|
97
|
+
tokenid: u.tokenid ?? '0x00',
|
|
98
|
+
token: u.token,
|
|
99
|
+
storestate: u.storestate,
|
|
100
|
+
state: u.state,
|
|
101
|
+
spent: !!u.spent,
|
|
102
|
+
mmrentry: u.mmrentry,
|
|
103
|
+
created: u.created,
|
|
104
|
+
}));
|
|
50
105
|
}
|
|
51
106
|
async getCoin(coinId) {
|
|
52
107
|
try {
|
|
53
|
-
const coins = await this.
|
|
54
|
-
|
|
108
|
+
const coins = await this.rpc(`coins coinid:${coinId}`);
|
|
109
|
+
const u = Array.isArray(coins) ? coins[0] : null;
|
|
110
|
+
if (!u)
|
|
111
|
+
return null;
|
|
112
|
+
return {
|
|
113
|
+
coinid: u.coinid ?? coinId,
|
|
114
|
+
amount: String(u.amount ?? '0'),
|
|
115
|
+
address: u.address ?? '',
|
|
116
|
+
miniaddress: u.miniaddress,
|
|
117
|
+
tokenid: u.tokenid ?? '0x00',
|
|
118
|
+
token: u.token,
|
|
119
|
+
storestate: u.storestate,
|
|
120
|
+
state: u.state,
|
|
121
|
+
spent: !!u.spent,
|
|
122
|
+
mmrentry: u.mmrentry,
|
|
123
|
+
created: u.created,
|
|
124
|
+
};
|
|
55
125
|
}
|
|
56
126
|
catch {
|
|
57
127
|
return null;
|
|
58
128
|
}
|
|
59
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Get MMR proof for a coin via Minima `coinproof` RPC command.
|
|
132
|
+
*/
|
|
60
133
|
async getProof(coinId) {
|
|
61
|
-
|
|
134
|
+
const data = await this.rpc(`coinproof coinid:${coinId}`);
|
|
135
|
+
return { coinid: coinId, data };
|
|
62
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Get chain tip via Minima `status` RPC — parses response.chain.
|
|
139
|
+
*/
|
|
63
140
|
async getTip() {
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
141
|
+
const data = await this.rpc('status');
|
|
142
|
+
const chain = data?.chain;
|
|
143
|
+
return {
|
|
144
|
+
block: chain?.block ?? 0,
|
|
145
|
+
hash: chain?.hash ?? '',
|
|
146
|
+
time: chain?.time,
|
|
147
|
+
};
|
|
69
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Get token info via Minima `tokens tokenid:X` RPC command.
|
|
151
|
+
*/
|
|
70
152
|
async getToken(tokenId) {
|
|
71
|
-
|
|
153
|
+
const data = await this.rpc(`tokens tokenid:${tokenId}`);
|
|
154
|
+
const t = Array.isArray(data) ? data[0] : data;
|
|
155
|
+
if (!t)
|
|
156
|
+
throw new Error(`Token not found: ${tokenId}`);
|
|
157
|
+
return this._mapToken(t);
|
|
72
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Search tokens via Minima `tokens` RPC command, then filter client-side.
|
|
161
|
+
*/
|
|
73
162
|
async searchTokens(query) {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
163
|
+
const data = await this.rpc('tokens');
|
|
164
|
+
const all = Array.isArray(data) ? data : [];
|
|
165
|
+
return all
|
|
166
|
+
.filter((t) => {
|
|
167
|
+
if (t.tokenid === '0x00')
|
|
168
|
+
return false; // skip native Minima
|
|
169
|
+
if (query.name) {
|
|
170
|
+
const n = typeof t.name === 'object' ? (t.name?.name ?? '') : String(t.name ?? '');
|
|
171
|
+
if (!n.toLowerCase().includes(query.name.toLowerCase()))
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
return true;
|
|
175
|
+
})
|
|
176
|
+
.slice(query.offset ?? 0, query.limit ? (query.offset ?? 0) + query.limit : undefined)
|
|
177
|
+
.map(this._mapToken);
|
|
88
178
|
}
|
|
89
179
|
async getTokensByCreator(address) {
|
|
90
180
|
return this.searchTokens({ creatorAddress: address });
|
|
91
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Broadcast a mined TxPoW hex.
|
|
184
|
+
* Uses POST /api/meg/postminedtxn (the correct Axia MEG broadcast bridge).
|
|
185
|
+
*/
|
|
92
186
|
async broadcastTxPoW(txpowHex) {
|
|
93
187
|
try {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
188
|
+
const controller = new AbortController();
|
|
189
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
190
|
+
let result;
|
|
191
|
+
try {
|
|
192
|
+
const res = await fetch(`${this.baseUrl}/api/meg/postminedtxn`, {
|
|
193
|
+
method: 'POST',
|
|
194
|
+
signal: controller.signal,
|
|
195
|
+
headers: { 'Content-Type': 'application/json' },
|
|
196
|
+
body: JSON.stringify({ data: txpowHex }),
|
|
197
|
+
});
|
|
198
|
+
result = await res.json();
|
|
199
|
+
if (!res.ok)
|
|
200
|
+
return { success: false, message: `HTTP ${res.status}: ${result?.error ?? ''}` };
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
clearTimeout(timer);
|
|
204
|
+
}
|
|
205
|
+
return { success: true, txpowid: result?.txpowid, message: result?.message };
|
|
99
206
|
}
|
|
100
207
|
catch (e) {
|
|
101
208
|
return { success: false, message: String(e) };
|
|
102
209
|
}
|
|
103
210
|
}
|
|
211
|
+
_mapToken(t) {
|
|
212
|
+
return {
|
|
213
|
+
tokenid: t.tokenid ?? '',
|
|
214
|
+
name: typeof t.name === 'object' ? t.name : { name: String(t.name ?? '') },
|
|
215
|
+
total: t.total,
|
|
216
|
+
confirmed: t.confirmed,
|
|
217
|
+
sendable: t.sendable,
|
|
218
|
+
coins: t.coins,
|
|
219
|
+
script: t.script,
|
|
220
|
+
description: t.description,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
104
223
|
}
|
|
224
|
+
exports.HostedProvider = HostedProvider;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LookupClientProvider — ChainStateProvider backed by a @totemsdk/lookup-client instance.
|
|
3
|
+
*
|
|
4
|
+
* Accepts any object that satisfies LookupClientLike (structural duck-typing),
|
|
5
|
+
* so @totemsdk/lookup-client remains a peer dependency: chain-provider does not
|
|
6
|
+
* bundle or import it at runtime. The caller is responsible for constructing the
|
|
7
|
+
* LookupClient and passing it in.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { connectLookupNode, LookupClientProvider } from '@totemsdk/lookup-client';
|
|
12
|
+
*
|
|
13
|
+
* const client = await connectLookupNode({ hyperswarmTopic: 'deadbeef...' });
|
|
14
|
+
* const provider = new LookupClientProvider(client);
|
|
15
|
+
*
|
|
16
|
+
* const tip = await provider.getTip();
|
|
17
|
+
* const coins = await provider.getCoins({ address: 'Mx...' });
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Use inside a CompositeProvider for sovereign-first / hosted-fallback:
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { CompositeProvider, HostedProvider, LookupClientProvider } from '@totemsdk/chain-provider';
|
|
24
|
+
* import { connectLookupNode } from '@totemsdk/lookup-client';
|
|
25
|
+
*
|
|
26
|
+
* const client = await connectLookupNode({ hyperswarmTopic: 'abc...' });
|
|
27
|
+
* const fallback = new HostedProvider({ baseUrl: 'https://api.axia.to', projectId: '...' });
|
|
28
|
+
*
|
|
29
|
+
* const provider = new CompositeProvider(
|
|
30
|
+
* new LookupClientProvider(client),
|
|
31
|
+
* fallback,
|
|
32
|
+
* );
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
import type { BroadcastResult, ChainStateProvider, ChainTip, Coin, CoinsQuery, MMRProof, TokenInfo, TokenSearchQuery } from '../types.js';
|
|
36
|
+
/**
|
|
37
|
+
* Structural interface describing the subset of LookupClient methods that
|
|
38
|
+
* LookupClientProvider requires. Any object satisfying this interface can be
|
|
39
|
+
* passed in — most commonly a `LookupClient` from @totemsdk/lookup-client.
|
|
40
|
+
*/
|
|
41
|
+
export interface LookupClientLike {
|
|
42
|
+
getCoins(query: CoinsQuery): Promise<Coin[]>;
|
|
43
|
+
getCoin(coinId: string): Promise<Coin | null>;
|
|
44
|
+
getProof(coinId: string): Promise<MMRProof>;
|
|
45
|
+
getTip(): Promise<ChainTip>;
|
|
46
|
+
getToken(tokenId: string): Promise<TokenInfo>;
|
|
47
|
+
searchTokens(query: TokenSearchQuery): Promise<TokenInfo[]>;
|
|
48
|
+
getTokensByCreator(address: string): Promise<TokenInfo[]>;
|
|
49
|
+
broadcastTxPoW(txpowHex: string): Promise<BroadcastResult>;
|
|
50
|
+
}
|
|
51
|
+
export declare class LookupClientProvider implements ChainStateProvider {
|
|
52
|
+
private readonly _client;
|
|
53
|
+
constructor(_client: LookupClientLike);
|
|
54
|
+
getCoins(query: CoinsQuery): Promise<Coin[]>;
|
|
55
|
+
getCoin(coinId: string): Promise<Coin | null>;
|
|
56
|
+
getProof(coinId: string): Promise<MMRProof>;
|
|
57
|
+
getTip(): Promise<ChainTip>;
|
|
58
|
+
getToken(tokenId: string): Promise<TokenInfo>;
|
|
59
|
+
searchTokens(query: TokenSearchQuery): Promise<TokenInfo[]>;
|
|
60
|
+
getTokensByCreator(address: string): Promise<TokenInfo[]>;
|
|
61
|
+
broadcastTxPoW(txpowHex: string): Promise<BroadcastResult>;
|
|
62
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* LookupClientProvider — ChainStateProvider backed by a @totemsdk/lookup-client instance.
|
|
4
|
+
*
|
|
5
|
+
* Accepts any object that satisfies LookupClientLike (structural duck-typing),
|
|
6
|
+
* so @totemsdk/lookup-client remains a peer dependency: chain-provider does not
|
|
7
|
+
* bundle or import it at runtime. The caller is responsible for constructing the
|
|
8
|
+
* LookupClient and passing it in.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { connectLookupNode, LookupClientProvider } from '@totemsdk/lookup-client';
|
|
13
|
+
*
|
|
14
|
+
* const client = await connectLookupNode({ hyperswarmTopic: 'deadbeef...' });
|
|
15
|
+
* const provider = new LookupClientProvider(client);
|
|
16
|
+
*
|
|
17
|
+
* const tip = await provider.getTip();
|
|
18
|
+
* const coins = await provider.getCoins({ address: 'Mx...' });
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* Use inside a CompositeProvider for sovereign-first / hosted-fallback:
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* import { CompositeProvider, HostedProvider, LookupClientProvider } from '@totemsdk/chain-provider';
|
|
25
|
+
* import { connectLookupNode } from '@totemsdk/lookup-client';
|
|
26
|
+
*
|
|
27
|
+
* const client = await connectLookupNode({ hyperswarmTopic: 'abc...' });
|
|
28
|
+
* const fallback = new HostedProvider({ baseUrl: 'https://api.axia.to', projectId: '...' });
|
|
29
|
+
*
|
|
30
|
+
* const provider = new CompositeProvider(
|
|
31
|
+
* new LookupClientProvider(client),
|
|
32
|
+
* fallback,
|
|
33
|
+
* );
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.LookupClientProvider = void 0;
|
|
38
|
+
class LookupClientProvider {
|
|
39
|
+
constructor(_client) {
|
|
40
|
+
this._client = _client;
|
|
41
|
+
}
|
|
42
|
+
getCoins(query) {
|
|
43
|
+
return this._client.getCoins(query);
|
|
44
|
+
}
|
|
45
|
+
getCoin(coinId) {
|
|
46
|
+
return this._client.getCoin(coinId);
|
|
47
|
+
}
|
|
48
|
+
getProof(coinId) {
|
|
49
|
+
return this._client.getProof(coinId);
|
|
50
|
+
}
|
|
51
|
+
getTip() {
|
|
52
|
+
return this._client.getTip();
|
|
53
|
+
}
|
|
54
|
+
getToken(tokenId) {
|
|
55
|
+
return this._client.getToken(tokenId);
|
|
56
|
+
}
|
|
57
|
+
searchTokens(query) {
|
|
58
|
+
return this._client.searchTokens(query);
|
|
59
|
+
}
|
|
60
|
+
getTokensByCreator(address) {
|
|
61
|
+
return this._client.getTokensByCreator(address);
|
|
62
|
+
}
|
|
63
|
+
broadcastTxPoW(txpowHex) {
|
|
64
|
+
return this._client.broadcastTxPoW(txpowHex);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
exports.LookupClientProvider = LookupClientProvider;
|
package/dist/providers/lookup.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/**
|
|
2
3
|
* LookupNodeProvider — stub.
|
|
3
4
|
*
|
|
4
5
|
* Full implementation lives in @totemsdk/lookup-client which replaces this
|
|
5
6
|
* stub once the lookup node package is available.
|
|
6
7
|
*/
|
|
7
|
-
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.LookupNodeProvider = exports.LookupNodeNotImplementedError = void 0;
|
|
10
|
+
class LookupNodeNotImplementedError extends Error {
|
|
8
11
|
constructor() {
|
|
9
12
|
super('LookupNodeProvider is not yet implemented. ' +
|
|
10
13
|
'Install @totemsdk/lookup-client and use LookupClientProvider instead. ' +
|
|
@@ -12,7 +15,8 @@ export class LookupNodeNotImplementedError extends Error {
|
|
|
12
15
|
this.name = 'LookupNodeNotImplementedError';
|
|
13
16
|
}
|
|
14
17
|
}
|
|
15
|
-
|
|
18
|
+
exports.LookupNodeNotImplementedError = LookupNodeNotImplementedError;
|
|
19
|
+
class LookupNodeProvider {
|
|
16
20
|
getCoins(_query) {
|
|
17
21
|
throw new LookupNodeNotImplementedError();
|
|
18
22
|
}
|
|
@@ -38,3 +42,4 @@ export class LookupNodeProvider {
|
|
|
38
42
|
throw new LookupNodeNotImplementedError();
|
|
39
43
|
}
|
|
40
44
|
}
|
|
45
|
+
exports.LookupNodeProvider = LookupNodeProvider;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/**
|
|
2
3
|
* PureMinimaRpcProvider — thin wrapper over @totemsdk/pureminima-rpc.
|
|
3
4
|
*/
|
|
4
|
-
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.PureMinimaRpcProvider = void 0;
|
|
7
|
+
class PureMinimaRpcProvider {
|
|
5
8
|
constructor(client) {
|
|
6
9
|
this.client = client;
|
|
7
10
|
}
|
|
@@ -78,3 +81,4 @@ export class PureMinimaRpcProvider {
|
|
|
78
81
|
}
|
|
79
82
|
}
|
|
80
83
|
}
|
|
84
|
+
exports.PureMinimaRpcProvider = PureMinimaRpcProvider;
|
package/dist/types.js
CHANGED
package/package.json
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@totemsdk/chain-provider",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Unified ChainStateProvider interface for Totem SDK — Hosted, PureMinima, Composite",
|
|
5
|
-
"type": "module",
|
|
6
5
|
"main": "dist/index.js",
|
|
7
6
|
"types": "dist/index.d.ts",
|
|
8
7
|
"exports": {
|
|
9
8
|
".": {
|
|
10
9
|
"types": "./dist/index.d.ts",
|
|
10
|
+
"require": "./dist/index.js",
|
|
11
11
|
"import": "./dist/index.js"
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"dist",
|
|
16
|
-
"
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
17
18
|
],
|
|
18
19
|
"dependencies": {
|
|
19
|
-
"@totemsdk/core": "1.0
|
|
20
|
-
"@totemsdk/pureminima-rpc": "0.1.
|
|
20
|
+
"@totemsdk/core": "1.1.0",
|
|
21
|
+
"@totemsdk/pureminima-rpc": "0.1.3"
|
|
21
22
|
},
|
|
22
23
|
"devDependencies": {
|
|
23
|
-
"@noble/hashes": "^
|
|
24
|
+
"@noble/hashes": "^2.2.0",
|
|
24
25
|
"@types/jest": "^29.0.0",
|
|
25
26
|
"@types/node": "^20.0.0",
|
|
26
27
|
"jest": "^29.0.0",
|
|
@@ -34,6 +35,31 @@
|
|
|
34
35
|
"access": "public"
|
|
35
36
|
},
|
|
36
37
|
"license": "MIT",
|
|
38
|
+
"author": "Totem SDK",
|
|
39
|
+
"homepage": "https://totemsdk.com",
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/MrGheek/axia-totem/issues"
|
|
42
|
+
},
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/MrGheek/axia-totem.git",
|
|
46
|
+
"directory": "packages/totem-sdk/packages/chain-provider"
|
|
47
|
+
},
|
|
48
|
+
"keywords": [
|
|
49
|
+
"totem",
|
|
50
|
+
"totemsdk",
|
|
51
|
+
"minima",
|
|
52
|
+
"blockchain",
|
|
53
|
+
"quantum-resistant",
|
|
54
|
+
"wots",
|
|
55
|
+
"kissvm",
|
|
56
|
+
"utxo",
|
|
57
|
+
"chain-provider",
|
|
58
|
+
"rpc",
|
|
59
|
+
"provider",
|
|
60
|
+
"chain-data",
|
|
61
|
+
"strategy-pattern"
|
|
62
|
+
],
|
|
37
63
|
"scripts": {
|
|
38
64
|
"build": "tsc",
|
|
39
65
|
"clean": "rm -rf dist",
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import { HostedProvider } from '../providers/hosted';
|
|
2
|
-
import { CompositeProvider } from '../providers/composite';
|
|
3
|
-
import { LookupNodeProvider, LookupNodeNotImplementedError } from '../providers/lookup';
|
|
4
|
-
import type { ChainStateProvider, ChainTip } from '../types';
|
|
5
|
-
|
|
6
|
-
function mockFetchJson(data: unknown, ok = true, status = 200) {
|
|
7
|
-
global.fetch = jest.fn().mockResolvedValue({
|
|
8
|
-
ok,
|
|
9
|
-
status,
|
|
10
|
-
json: () => Promise.resolve(data),
|
|
11
|
-
}) as unknown as typeof fetch;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
afterEach(() => jest.restoreAllMocks());
|
|
15
|
-
|
|
16
|
-
describe('HostedProvider', () => {
|
|
17
|
-
const provider = new HostedProvider({ baseUrl: 'https://api.axia.to', apiKey: 'test-key' });
|
|
18
|
-
|
|
19
|
-
it('getCoins passes query params', async () => {
|
|
20
|
-
mockFetchJson([]);
|
|
21
|
-
await provider.getCoins({ address: 'Mx123', sendable: true });
|
|
22
|
-
const url = (global.fetch as jest.Mock).mock.calls[0][0] as string;
|
|
23
|
-
expect(url).toContain('address=Mx123');
|
|
24
|
-
expect(url).toContain('sendable=true');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('getCoins unwraps { coins: [] } response shape', async () => {
|
|
28
|
-
mockFetchJson({ coins: [{ coinid: '0xABC' }] });
|
|
29
|
-
const coins = await provider.getCoins({});
|
|
30
|
-
expect(coins[0].coinid).toBe('0xABC');
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('getTip handles flat ChainTip response', async () => {
|
|
34
|
-
mockFetchJson({ block: 100, hash: '0xHASH', time: '2024-01-01' });
|
|
35
|
-
const tip = await provider.getTip();
|
|
36
|
-
expect(tip.block).toBe(100);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('getTip unwraps nested chain.block response', async () => {
|
|
40
|
-
mockFetchJson({ chain: { block: 200, hash: '0xHASH2' } });
|
|
41
|
-
const tip = await provider.getTip();
|
|
42
|
-
expect(tip.block).toBe(200);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('broadcastTxPoW returns success:true on ok response', async () => {
|
|
46
|
-
mockFetchJson({ txpowid: '0xTXID' });
|
|
47
|
-
const result = await provider.broadcastTxPoW('0xDEAD');
|
|
48
|
-
expect(result.success).toBe(true);
|
|
49
|
-
expect(result.txpowid).toBe('0xTXID');
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('broadcastTxPoW returns success:false on fetch error', async () => {
|
|
53
|
-
global.fetch = jest.fn().mockRejectedValue(new Error('network down')) as unknown as typeof fetch;
|
|
54
|
-
const result = await provider.broadcastTxPoW('0xDEAD');
|
|
55
|
-
expect(result.success).toBe(false);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('attaches x-api-key header', async () => {
|
|
59
|
-
mockFetchJson({ block: 1, hash: '0x' });
|
|
60
|
-
await provider.getTip();
|
|
61
|
-
const headers = (global.fetch as jest.Mock).mock.calls[0][1].headers as Record<string, string>;
|
|
62
|
-
expect(headers['x-api-key']).toBe('test-key');
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
describe('CompositeProvider', () => {
|
|
67
|
-
function makeProvider(tip: ChainTip, shouldFail = false): ChainStateProvider {
|
|
68
|
-
return {
|
|
69
|
-
getCoins: jest.fn().mockResolvedValue([]),
|
|
70
|
-
getCoin: jest.fn().mockResolvedValue(null),
|
|
71
|
-
getProof: jest.fn().mockResolvedValue({}),
|
|
72
|
-
getTip: shouldFail
|
|
73
|
-
? jest.fn().mockRejectedValue(new Error('primary down'))
|
|
74
|
-
: jest.fn().mockResolvedValue(tip),
|
|
75
|
-
getToken: jest.fn().mockResolvedValue({}),
|
|
76
|
-
searchTokens: jest.fn().mockResolvedValue([]),
|
|
77
|
-
getTokensByCreator: jest.fn().mockResolvedValue([]),
|
|
78
|
-
broadcastTxPoW: jest.fn().mockResolvedValue({ success: true }),
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
it('uses primary when it succeeds', async () => {
|
|
83
|
-
const primary = makeProvider({ block: 10, hash: '0xA' });
|
|
84
|
-
const fallback = makeProvider({ block: 5, hash: '0xB' });
|
|
85
|
-
const composite = new CompositeProvider(primary, fallback);
|
|
86
|
-
const tip = await composite.getTip();
|
|
87
|
-
expect(tip.block).toBe(10);
|
|
88
|
-
expect(fallback.getTip).not.toHaveBeenCalled();
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('falls back to secondary when primary fails', async () => {
|
|
92
|
-
const primary = makeProvider({ block: 10, hash: '0xA' }, true);
|
|
93
|
-
const fallback = makeProvider({ block: 5, hash: '0xB' });
|
|
94
|
-
const composite = new CompositeProvider(primary, fallback);
|
|
95
|
-
const tip = await composite.getTip();
|
|
96
|
-
expect(tip.block).toBe(5);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('calls onFallback callback when falling back', async () => {
|
|
100
|
-
const onFallback = jest.fn();
|
|
101
|
-
const primary = makeProvider({ block: 10, hash: '0xA' }, true);
|
|
102
|
-
const fallback = makeProvider({ block: 5, hash: '0xB' });
|
|
103
|
-
const composite = new CompositeProvider(primary, fallback, onFallback);
|
|
104
|
-
await composite.getTip();
|
|
105
|
-
expect(onFallback).toHaveBeenCalledWith('getTip', expect.any(Error));
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
describe('LookupNodeProvider', () => {
|
|
110
|
-
it('throws LookupNodeNotImplementedError on all methods', () => {
|
|
111
|
-
const p = new LookupNodeProvider();
|
|
112
|
-
expect(() => p.getTip()).toThrow(LookupNodeNotImplementedError);
|
|
113
|
-
expect(() => p.getCoins({})).toThrow(LookupNodeNotImplementedError);
|
|
114
|
-
expect(() => p.broadcastTxPoW('0x')).toThrow(LookupNodeNotImplementedError);
|
|
115
|
-
});
|
|
116
|
-
});
|
package/src/index.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export type {
|
|
2
|
-
ChainStateProvider,
|
|
3
|
-
CoinsQuery,
|
|
4
|
-
Coin,
|
|
5
|
-
MMRProof,
|
|
6
|
-
ChainTip,
|
|
7
|
-
TokenInfo,
|
|
8
|
-
TokenSearchQuery,
|
|
9
|
-
BroadcastResult,
|
|
10
|
-
} from './types.js';
|
|
11
|
-
|
|
12
|
-
export { HostedProvider } from './providers/hosted.js';
|
|
13
|
-
export type { HostedProviderConfig } from './providers/hosted.js';
|
|
14
|
-
|
|
15
|
-
export { PureMinimaRpcProvider } from './providers/pureminima.js';
|
|
16
|
-
|
|
17
|
-
export { LookupNodeProvider, LookupNodeNotImplementedError } from './providers/lookup.js';
|
|
18
|
-
|
|
19
|
-
export { CompositeProvider } from './providers/composite.js';
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CompositeProvider — tries primary provider, falls back to secondary on any error.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type {
|
|
6
|
-
ChainStateProvider,
|
|
7
|
-
CoinsQuery,
|
|
8
|
-
Coin,
|
|
9
|
-
MMRProof,
|
|
10
|
-
ChainTip,
|
|
11
|
-
TokenInfo,
|
|
12
|
-
TokenSearchQuery,
|
|
13
|
-
BroadcastResult,
|
|
14
|
-
} from '../types.js';
|
|
15
|
-
|
|
16
|
-
export class CompositeProvider implements ChainStateProvider {
|
|
17
|
-
constructor(
|
|
18
|
-
private readonly primary: ChainStateProvider,
|
|
19
|
-
private readonly fallback: ChainStateProvider,
|
|
20
|
-
private readonly onFallback?: (method: string, error: unknown) => void,
|
|
21
|
-
) {}
|
|
22
|
-
|
|
23
|
-
private withArg<T>(method: string, arg: unknown): Promise<T> {
|
|
24
|
-
const p = this.primary as unknown as Record<string, (a: unknown) => Promise<T>>;
|
|
25
|
-
const f = this.fallback as unknown as Record<string, (a: unknown) => Promise<T>>;
|
|
26
|
-
return p[method](arg).catch((err: unknown) => {
|
|
27
|
-
this.onFallback?.(method, err);
|
|
28
|
-
return f[method](arg);
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
private withNoArg<T>(method: string): Promise<T> {
|
|
33
|
-
const p = this.primary as unknown as Record<string, () => Promise<T>>;
|
|
34
|
-
const f = this.fallback as unknown as Record<string, () => Promise<T>>;
|
|
35
|
-
return p[method]().catch((err: unknown) => {
|
|
36
|
-
this.onFallback?.(method, err);
|
|
37
|
-
return f[method]();
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
getCoins(query: CoinsQuery): Promise<Coin[]> {
|
|
42
|
-
return this.withArg('getCoins', query);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
getCoin(coinId: string): Promise<Coin | null> {
|
|
46
|
-
return this.withArg('getCoin', coinId);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
getProof(coinId: string): Promise<MMRProof> {
|
|
50
|
-
return this.withArg('getProof', coinId);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
getTip(): Promise<ChainTip> {
|
|
54
|
-
return this.withNoArg('getTip');
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
getToken(tokenId: string): Promise<TokenInfo> {
|
|
58
|
-
return this.withArg('getToken', tokenId);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
searchTokens(query: TokenSearchQuery): Promise<TokenInfo[]> {
|
|
62
|
-
return this.withArg('searchTokens', query);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
getTokensByCreator(address: string): Promise<TokenInfo[]> {
|
|
66
|
-
return this.withArg('getTokensByCreator', address);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
broadcastTxPoW(txpowHex: string): Promise<BroadcastResult> {
|
|
70
|
-
return this.withArg('broadcastTxPoW', txpowHex);
|
|
71
|
-
}
|
|
72
|
-
}
|
package/src/providers/hosted.ts
DELETED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HostedProvider — wraps existing Axia REST API endpoints.
|
|
3
|
-
* Uses fetch only; no Node.js-specific dependencies.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type {
|
|
7
|
-
ChainStateProvider,
|
|
8
|
-
CoinsQuery,
|
|
9
|
-
Coin,
|
|
10
|
-
MMRProof,
|
|
11
|
-
ChainTip,
|
|
12
|
-
TokenInfo,
|
|
13
|
-
TokenSearchQuery,
|
|
14
|
-
BroadcastResult,
|
|
15
|
-
} from '../types.js';
|
|
16
|
-
|
|
17
|
-
export interface HostedProviderConfig {
|
|
18
|
-
baseUrl: string;
|
|
19
|
-
apiKey: string;
|
|
20
|
-
timeoutMs?: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export class HostedProvider implements ChainStateProvider {
|
|
24
|
-
private readonly baseUrl: string;
|
|
25
|
-
private readonly apiKey: string;
|
|
26
|
-
private readonly timeoutMs: number;
|
|
27
|
-
|
|
28
|
-
constructor(config: HostedProviderConfig) {
|
|
29
|
-
this.baseUrl = config.baseUrl.replace(/\/$/, '');
|
|
30
|
-
this.apiKey = config.apiKey;
|
|
31
|
-
this.timeoutMs = config.timeoutMs ?? 30_000;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
private async fetchJson<T>(path: string, init?: RequestInit): Promise<T> {
|
|
35
|
-
const controller = new AbortController();
|
|
36
|
-
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
37
|
-
try {
|
|
38
|
-
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
39
|
-
...init,
|
|
40
|
-
signal: controller.signal,
|
|
41
|
-
headers: {
|
|
42
|
-
'x-api-key': this.apiKey,
|
|
43
|
-
'Content-Type': 'application/json',
|
|
44
|
-
...((init?.headers as Record<string, string>) ?? {}),
|
|
45
|
-
},
|
|
46
|
-
});
|
|
47
|
-
if (!res.ok) {
|
|
48
|
-
throw new Error(`HostedProvider HTTP ${res.status} for ${path}`);
|
|
49
|
-
}
|
|
50
|
-
return res.json() as Promise<T>;
|
|
51
|
-
} finally {
|
|
52
|
-
clearTimeout(timer);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async getCoins(query: CoinsQuery): Promise<Coin[]> {
|
|
57
|
-
const params = new URLSearchParams();
|
|
58
|
-
if (query.address) params.set('address', query.address);
|
|
59
|
-
if (query.tokenId) params.set('tokenid', query.tokenId);
|
|
60
|
-
if (query.sendable !== undefined) params.set('sendable', String(query.sendable));
|
|
61
|
-
if (query.relevant !== undefined) params.set('relevant', String(query.relevant));
|
|
62
|
-
if (query.coinId) params.set('coinid', query.coinId);
|
|
63
|
-
if (query.megammr !== undefined) params.set('megammr', String(query.megammr));
|
|
64
|
-
const qs = params.toString();
|
|
65
|
-
const result = await this.fetchJson<{ coins?: Coin[] } | Coin[]>(
|
|
66
|
-
`/v1/wallet/coins${qs ? `?${qs}` : ''}`,
|
|
67
|
-
);
|
|
68
|
-
return Array.isArray(result) ? result : (result.coins ?? []);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async getCoin(coinId: string): Promise<Coin | null> {
|
|
72
|
-
try {
|
|
73
|
-
const coins = await this.getCoins({ coinId });
|
|
74
|
-
return coins[0] ?? null;
|
|
75
|
-
} catch {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async getProof(coinId: string): Promise<MMRProof> {
|
|
81
|
-
return this.fetchJson<MMRProof>(`/v1/wallet/proofs/${encodeURIComponent(coinId)}`);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async getTip(): Promise<ChainTip> {
|
|
85
|
-
const result = await this.fetchJson<{ chain?: ChainTip; tip?: ChainTip } | ChainTip>(
|
|
86
|
-
'/v1/status/tip',
|
|
87
|
-
);
|
|
88
|
-
if ('block' in result) return result as ChainTip;
|
|
89
|
-
const r = result as { chain?: ChainTip; tip?: ChainTip };
|
|
90
|
-
return (r.chain ?? r.tip) as ChainTip;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async getToken(tokenId: string): Promise<TokenInfo> {
|
|
94
|
-
return this.fetchJson<TokenInfo>(`/v1/tokens/${encodeURIComponent(tokenId)}`);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async searchTokens(query: TokenSearchQuery): Promise<TokenInfo[]> {
|
|
98
|
-
const params = new URLSearchParams();
|
|
99
|
-
if (query.name) params.set('name', query.name);
|
|
100
|
-
if (query.category?.length) params.set('category', query.category.join(','));
|
|
101
|
-
if (query.creatorAddress) params.set('creator', query.creatorAddress);
|
|
102
|
-
if (query.limit !== undefined) params.set('limit', String(query.limit));
|
|
103
|
-
if (query.offset !== undefined) params.set('offset', String(query.offset));
|
|
104
|
-
const qs = params.toString();
|
|
105
|
-
const result = await this.fetchJson<{ tokens?: TokenInfo[] } | TokenInfo[]>(
|
|
106
|
-
`/v1/tokens${qs ? `?${qs}` : ''}`,
|
|
107
|
-
);
|
|
108
|
-
return Array.isArray(result) ? result : (result.tokens ?? []);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async getTokensByCreator(address: string): Promise<TokenInfo[]> {
|
|
112
|
-
return this.searchTokens({ creatorAddress: address });
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async broadcastTxPoW(txpowHex: string): Promise<BroadcastResult> {
|
|
116
|
-
try {
|
|
117
|
-
const result = await this.fetchJson<{ txpowid?: string; message?: string }>(
|
|
118
|
-
'/v1/txpow/broadcast',
|
|
119
|
-
{
|
|
120
|
-
method: 'POST',
|
|
121
|
-
body: JSON.stringify({ txpow: txpowHex }),
|
|
122
|
-
},
|
|
123
|
-
);
|
|
124
|
-
return { success: true, txpowid: result.txpowid, message: result.message };
|
|
125
|
-
} catch (e) {
|
|
126
|
-
return { success: false, message: String(e) };
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
package/src/providers/lookup.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LookupNodeProvider — stub.
|
|
3
|
-
*
|
|
4
|
-
* Full implementation lives in @totemsdk/lookup-client which replaces this
|
|
5
|
-
* stub once the lookup node package is available.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type {
|
|
9
|
-
ChainStateProvider,
|
|
10
|
-
CoinsQuery,
|
|
11
|
-
Coin,
|
|
12
|
-
MMRProof,
|
|
13
|
-
ChainTip,
|
|
14
|
-
TokenInfo,
|
|
15
|
-
TokenSearchQuery,
|
|
16
|
-
BroadcastResult,
|
|
17
|
-
} from '../types.js';
|
|
18
|
-
|
|
19
|
-
export class LookupNodeNotImplementedError extends Error {
|
|
20
|
-
constructor() {
|
|
21
|
-
super(
|
|
22
|
-
'LookupNodeProvider is not yet implemented. ' +
|
|
23
|
-
'Install @totemsdk/lookup-client and use LookupClientProvider instead. ' +
|
|
24
|
-
'See https://github.com/Totem/totem-sdk for setup instructions.',
|
|
25
|
-
);
|
|
26
|
-
this.name = 'LookupNodeNotImplementedError';
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export class LookupNodeProvider implements ChainStateProvider {
|
|
31
|
-
getCoins(_query: CoinsQuery): Promise<Coin[]> {
|
|
32
|
-
throw new LookupNodeNotImplementedError();
|
|
33
|
-
}
|
|
34
|
-
getCoin(_coinId: string): Promise<Coin | null> {
|
|
35
|
-
throw new LookupNodeNotImplementedError();
|
|
36
|
-
}
|
|
37
|
-
getProof(_coinId: string): Promise<MMRProof> {
|
|
38
|
-
throw new LookupNodeNotImplementedError();
|
|
39
|
-
}
|
|
40
|
-
getTip(): Promise<ChainTip> {
|
|
41
|
-
throw new LookupNodeNotImplementedError();
|
|
42
|
-
}
|
|
43
|
-
getToken(_tokenId: string): Promise<TokenInfo> {
|
|
44
|
-
throw new LookupNodeNotImplementedError();
|
|
45
|
-
}
|
|
46
|
-
searchTokens(_query: TokenSearchQuery): Promise<TokenInfo[]> {
|
|
47
|
-
throw new LookupNodeNotImplementedError();
|
|
48
|
-
}
|
|
49
|
-
getTokensByCreator(_address: string): Promise<TokenInfo[]> {
|
|
50
|
-
throw new LookupNodeNotImplementedError();
|
|
51
|
-
}
|
|
52
|
-
broadcastTxPoW(_txpowHex: string): Promise<BroadcastResult> {
|
|
53
|
-
throw new LookupNodeNotImplementedError();
|
|
54
|
-
}
|
|
55
|
-
}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PureMinimaRpcProvider — thin wrapper over @totemsdk/pureminima-rpc.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { PureMinimaClient } from '@totemsdk/pureminima-rpc';
|
|
6
|
-
import type {
|
|
7
|
-
ChainStateProvider,
|
|
8
|
-
CoinsQuery,
|
|
9
|
-
Coin,
|
|
10
|
-
MMRProof,
|
|
11
|
-
ChainTip,
|
|
12
|
-
TokenInfo,
|
|
13
|
-
TokenSearchQuery,
|
|
14
|
-
BroadcastResult,
|
|
15
|
-
} from '../types.js';
|
|
16
|
-
|
|
17
|
-
export class PureMinimaRpcProvider implements ChainStateProvider {
|
|
18
|
-
constructor(private readonly client: PureMinimaClient) {}
|
|
19
|
-
|
|
20
|
-
async getCoins(query: CoinsQuery): Promise<Coin[]> {
|
|
21
|
-
const result = await this.client.coins({
|
|
22
|
-
address: query.address,
|
|
23
|
-
tokenid: query.tokenId,
|
|
24
|
-
sendable: query.sendable,
|
|
25
|
-
relevant: query.relevant,
|
|
26
|
-
coinid: query.coinId,
|
|
27
|
-
megammr: query.megammr,
|
|
28
|
-
});
|
|
29
|
-
return result as unknown as Coin[];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async getCoin(coinId: string): Promise<Coin | null> {
|
|
33
|
-
try {
|
|
34
|
-
const coins = await this.getCoins({ coinId });
|
|
35
|
-
return coins[0] ?? null;
|
|
36
|
-
} catch {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async getProof(coinId: string): Promise<MMRProof> {
|
|
42
|
-
return this.client.mmrProof(coinId) as unknown as MMRProof;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async getTip(): Promise<ChainTip> {
|
|
46
|
-
return this.client.getTip() as unknown as ChainTip;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async getToken(tokenId: string): Promise<TokenInfo> {
|
|
50
|
-
const tokens = await this.client.tokens(tokenId);
|
|
51
|
-
if (!tokens || tokens.length === 0) {
|
|
52
|
-
throw new Error(`Token not found: ${tokenId}`);
|
|
53
|
-
}
|
|
54
|
-
return tokens[0] as unknown as TokenInfo;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async searchTokens(query: TokenSearchQuery): Promise<TokenInfo[]> {
|
|
58
|
-
const all = await this.client.tokens();
|
|
59
|
-
let results = all as unknown as TokenInfo[];
|
|
60
|
-
if (query.name) {
|
|
61
|
-
const needle = query.name.toLowerCase();
|
|
62
|
-
results = results.filter((t) => {
|
|
63
|
-
const nameStr = JSON.stringify(t.name ?? '').toLowerCase();
|
|
64
|
-
return nameStr.includes(needle);
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
if (query.creatorAddress) {
|
|
68
|
-
const addr = query.creatorAddress;
|
|
69
|
-
results = results.filter((t) => {
|
|
70
|
-
const desc = JSON.stringify(t.description ?? '');
|
|
71
|
-
return desc.includes(addr);
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
if (query.offset) results = results.slice(query.offset);
|
|
75
|
-
if (query.limit) results = results.slice(0, query.limit);
|
|
76
|
-
return results;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async getTokensByCreator(address: string): Promise<TokenInfo[]> {
|
|
80
|
-
return this.searchTokens({ creatorAddress: address });
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async broadcastTxPoW(txpowHex: string): Promise<BroadcastResult> {
|
|
84
|
-
try {
|
|
85
|
-
const result = await this.client.txnMinePost(txpowHex);
|
|
86
|
-
return {
|
|
87
|
-
success: true,
|
|
88
|
-
txpowid: result?.txpowid,
|
|
89
|
-
message: 'broadcast via PureMinima txnminepost',
|
|
90
|
-
};
|
|
91
|
-
} catch (e) {
|
|
92
|
-
return { success: false, message: String(e) };
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @totemsdk/chain-provider — shared types and ChainStateProvider interface
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export interface CoinsQuery {
|
|
6
|
-
address?: string;
|
|
7
|
-
tokenId?: string;
|
|
8
|
-
sendable?: boolean;
|
|
9
|
-
relevant?: boolean;
|
|
10
|
-
coinId?: string;
|
|
11
|
-
megammr?: boolean;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface Coin {
|
|
15
|
-
coinid: string;
|
|
16
|
-
amount: string;
|
|
17
|
-
address: string;
|
|
18
|
-
miniaddress?: string;
|
|
19
|
-
tokenid: string;
|
|
20
|
-
token?: unknown;
|
|
21
|
-
storestate?: boolean;
|
|
22
|
-
state?: unknown[];
|
|
23
|
-
spent?: boolean;
|
|
24
|
-
mmrentry?: string;
|
|
25
|
-
created?: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface MMRProof {
|
|
29
|
-
coinid: string;
|
|
30
|
-
data: unknown;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface ChainTip {
|
|
34
|
-
block: number;
|
|
35
|
-
hash: string;
|
|
36
|
-
time?: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface TokenInfo {
|
|
40
|
-
tokenid: string;
|
|
41
|
-
name: Record<string, unknown>;
|
|
42
|
-
total?: string;
|
|
43
|
-
confirmed?: string;
|
|
44
|
-
sendable?: string;
|
|
45
|
-
coins?: number;
|
|
46
|
-
script?: string;
|
|
47
|
-
description?: unknown;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface TokenSearchQuery {
|
|
51
|
-
name?: string;
|
|
52
|
-
category?: string[];
|
|
53
|
-
creatorAddress?: string;
|
|
54
|
-
limit?: number;
|
|
55
|
-
offset?: number;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface BroadcastResult {
|
|
59
|
-
txpowid?: string;
|
|
60
|
-
success: boolean;
|
|
61
|
-
message?: string;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface ChainStateProvider {
|
|
65
|
-
getCoins(query: CoinsQuery): Promise<Coin[]>;
|
|
66
|
-
getCoin(coinId: string): Promise<Coin | null>;
|
|
67
|
-
getProof(coinId: string): Promise<MMRProof>;
|
|
68
|
-
getTip(): Promise<ChainTip>;
|
|
69
|
-
getToken(tokenId: string): Promise<TokenInfo>;
|
|
70
|
-
searchTokens(query: TokenSearchQuery): Promise<TokenInfo[]>;
|
|
71
|
-
getTokensByCreator(address: string): Promise<TokenInfo[]>;
|
|
72
|
-
broadcastTxPoW(txpowHex: string): Promise<BroadcastResult>;
|
|
73
|
-
}
|