@totemsdk/chain-provider 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ export type { ChainStateProvider, CoinsQuery, Coin, MMRProof, ChainTip, TokenInfo, TokenSearchQuery, BroadcastResult, } from './types.js';
2
+ export { HostedProvider } from './providers/hosted.js';
3
+ export type { HostedProviderConfig } from './providers/hosted.js';
4
+ export { PureMinimaRpcProvider } from './providers/pureminima.js';
5
+ export { LookupNodeProvider, LookupNodeNotImplementedError } from './providers/lookup.js';
6
+ export { CompositeProvider } from './providers/composite.js';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { HostedProvider } from './providers/hosted.js';
2
+ export { PureMinimaRpcProvider } from './providers/pureminima.js';
3
+ export { LookupNodeProvider, LookupNodeNotImplementedError } from './providers/lookup.js';
4
+ export { CompositeProvider } from './providers/composite.js';
@@ -0,0 +1,20 @@
1
+ /**
2
+ * CompositeProvider — tries primary provider, falls back to secondary on any error.
3
+ */
4
+ import type { ChainStateProvider, CoinsQuery, Coin, MMRProof, ChainTip, TokenInfo, TokenSearchQuery, BroadcastResult } from '../types.js';
5
+ export declare class CompositeProvider implements ChainStateProvider {
6
+ private readonly primary;
7
+ private readonly fallback;
8
+ private readonly onFallback?;
9
+ constructor(primary: ChainStateProvider, fallback: ChainStateProvider, onFallback?: ((method: string, error: unknown) => void) | undefined);
10
+ private withArg;
11
+ private withNoArg;
12
+ getCoins(query: CoinsQuery): Promise<Coin[]>;
13
+ getCoin(coinId: string): Promise<Coin | null>;
14
+ getProof(coinId: string): Promise<MMRProof>;
15
+ getTip(): Promise<ChainTip>;
16
+ getToken(tokenId: string): Promise<TokenInfo>;
17
+ searchTokens(query: TokenSearchQuery): Promise<TokenInfo[]>;
18
+ getTokensByCreator(address: string): Promise<TokenInfo[]>;
19
+ broadcastTxPoW(txpowHex: string): Promise<BroadcastResult>;
20
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * CompositeProvider — tries primary provider, falls back to secondary on any error.
3
+ */
4
+ export class CompositeProvider {
5
+ constructor(primary, fallback, onFallback) {
6
+ this.primary = primary;
7
+ this.fallback = fallback;
8
+ this.onFallback = onFallback;
9
+ }
10
+ withArg(method, arg) {
11
+ const p = this.primary;
12
+ const f = this.fallback;
13
+ return p[method](arg).catch((err) => {
14
+ this.onFallback?.(method, err);
15
+ return f[method](arg);
16
+ });
17
+ }
18
+ withNoArg(method) {
19
+ const p = this.primary;
20
+ const f = this.fallback;
21
+ return p[method]().catch((err) => {
22
+ this.onFallback?.(method, err);
23
+ return f[method]();
24
+ });
25
+ }
26
+ getCoins(query) {
27
+ return this.withArg('getCoins', query);
28
+ }
29
+ getCoin(coinId) {
30
+ return this.withArg('getCoin', coinId);
31
+ }
32
+ getProof(coinId) {
33
+ return this.withArg('getProof', coinId);
34
+ }
35
+ getTip() {
36
+ return this.withNoArg('getTip');
37
+ }
38
+ getToken(tokenId) {
39
+ return this.withArg('getToken', tokenId);
40
+ }
41
+ searchTokens(query) {
42
+ return this.withArg('searchTokens', query);
43
+ }
44
+ getTokensByCreator(address) {
45
+ return this.withArg('getTokensByCreator', address);
46
+ }
47
+ broadcastTxPoW(txpowHex) {
48
+ return this.withArg('broadcastTxPoW', txpowHex);
49
+ }
50
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * HostedProvider — wraps existing Axia REST API endpoints.
3
+ * Uses fetch only; no Node.js-specific dependencies.
4
+ */
5
+ import type { ChainStateProvider, CoinsQuery, Coin, MMRProof, ChainTip, TokenInfo, TokenSearchQuery, BroadcastResult } from '../types.js';
6
+ export interface HostedProviderConfig {
7
+ baseUrl: string;
8
+ apiKey: string;
9
+ timeoutMs?: number;
10
+ }
11
+ export declare class HostedProvider implements ChainStateProvider {
12
+ private readonly baseUrl;
13
+ private readonly apiKey;
14
+ private readonly timeoutMs;
15
+ constructor(config: HostedProviderConfig);
16
+ private fetchJson;
17
+ getCoins(query: CoinsQuery): Promise<Coin[]>;
18
+ getCoin(coinId: string): Promise<Coin | null>;
19
+ getProof(coinId: string): Promise<MMRProof>;
20
+ getTip(): Promise<ChainTip>;
21
+ getToken(tokenId: string): Promise<TokenInfo>;
22
+ searchTokens(query: TokenSearchQuery): Promise<TokenInfo[]>;
23
+ getTokensByCreator(address: string): Promise<TokenInfo[]>;
24
+ broadcastTxPoW(txpowHex: string): Promise<BroadcastResult>;
25
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * HostedProvider — wraps existing Axia REST API endpoints.
3
+ * Uses fetch only; no Node.js-specific dependencies.
4
+ */
5
+ export class HostedProvider {
6
+ constructor(config) {
7
+ this.baseUrl = config.baseUrl.replace(/\/$/, '');
8
+ this.apiKey = config.apiKey;
9
+ this.timeoutMs = config.timeoutMs ?? 30000;
10
+ }
11
+ async fetchJson(path, init) {
12
+ const controller = new AbortController();
13
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
14
+ try {
15
+ const res = await fetch(`${this.baseUrl}${path}`, {
16
+ ...init,
17
+ signal: controller.signal,
18
+ headers: {
19
+ 'x-api-key': this.apiKey,
20
+ 'Content-Type': 'application/json',
21
+ ...(init?.headers ?? {}),
22
+ },
23
+ });
24
+ if (!res.ok) {
25
+ throw new Error(`HostedProvider HTTP ${res.status} for ${path}`);
26
+ }
27
+ return res.json();
28
+ }
29
+ finally {
30
+ clearTimeout(timer);
31
+ }
32
+ }
33
+ async getCoins(query) {
34
+ const params = new URLSearchParams();
35
+ if (query.address)
36
+ params.set('address', query.address);
37
+ if (query.tokenId)
38
+ params.set('tokenid', query.tokenId);
39
+ if (query.sendable !== undefined)
40
+ params.set('sendable', String(query.sendable));
41
+ if (query.relevant !== undefined)
42
+ params.set('relevant', String(query.relevant));
43
+ if (query.coinId)
44
+ params.set('coinid', query.coinId);
45
+ if (query.megammr !== undefined)
46
+ params.set('megammr', String(query.megammr));
47
+ const qs = params.toString();
48
+ const result = await this.fetchJson(`/v1/wallet/coins${qs ? `?${qs}` : ''}`);
49
+ return Array.isArray(result) ? result : (result.coins ?? []);
50
+ }
51
+ async getCoin(coinId) {
52
+ try {
53
+ const coins = await this.getCoins({ coinId });
54
+ return coins[0] ?? null;
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ }
60
+ async getProof(coinId) {
61
+ return this.fetchJson(`/v1/wallet/proofs/${encodeURIComponent(coinId)}`);
62
+ }
63
+ async getTip() {
64
+ const result = await this.fetchJson('/v1/status/tip');
65
+ if ('block' in result)
66
+ return result;
67
+ const r = result;
68
+ return (r.chain ?? r.tip);
69
+ }
70
+ async getToken(tokenId) {
71
+ return this.fetchJson(`/v1/tokens/${encodeURIComponent(tokenId)}`);
72
+ }
73
+ async searchTokens(query) {
74
+ const params = new URLSearchParams();
75
+ if (query.name)
76
+ params.set('name', query.name);
77
+ if (query.category?.length)
78
+ params.set('category', query.category.join(','));
79
+ if (query.creatorAddress)
80
+ params.set('creator', query.creatorAddress);
81
+ if (query.limit !== undefined)
82
+ params.set('limit', String(query.limit));
83
+ if (query.offset !== undefined)
84
+ params.set('offset', String(query.offset));
85
+ const qs = params.toString();
86
+ const result = await this.fetchJson(`/v1/tokens${qs ? `?${qs}` : ''}`);
87
+ return Array.isArray(result) ? result : (result.tokens ?? []);
88
+ }
89
+ async getTokensByCreator(address) {
90
+ return this.searchTokens({ creatorAddress: address });
91
+ }
92
+ async broadcastTxPoW(txpowHex) {
93
+ try {
94
+ const result = await this.fetchJson('/v1/txpow/broadcast', {
95
+ method: 'POST',
96
+ body: JSON.stringify({ txpow: txpowHex }),
97
+ });
98
+ return { success: true, txpowid: result.txpowid, message: result.message };
99
+ }
100
+ catch (e) {
101
+ return { success: false, message: String(e) };
102
+ }
103
+ }
104
+ }
@@ -0,0 +1,20 @@
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
+ import type { ChainStateProvider, CoinsQuery, Coin, MMRProof, ChainTip, TokenInfo, TokenSearchQuery, BroadcastResult } from '../types.js';
8
+ export declare class LookupNodeNotImplementedError extends Error {
9
+ constructor();
10
+ }
11
+ export declare class LookupNodeProvider implements ChainStateProvider {
12
+ getCoins(_query: CoinsQuery): Promise<Coin[]>;
13
+ getCoin(_coinId: string): Promise<Coin | null>;
14
+ getProof(_coinId: string): Promise<MMRProof>;
15
+ getTip(): Promise<ChainTip>;
16
+ getToken(_tokenId: string): Promise<TokenInfo>;
17
+ searchTokens(_query: TokenSearchQuery): Promise<TokenInfo[]>;
18
+ getTokensByCreator(_address: string): Promise<TokenInfo[]>;
19
+ broadcastTxPoW(_txpowHex: string): Promise<BroadcastResult>;
20
+ }
@@ -0,0 +1,40 @@
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
+ export class LookupNodeNotImplementedError extends Error {
8
+ constructor() {
9
+ super('LookupNodeProvider is not yet implemented. ' +
10
+ 'Install @totemsdk/lookup-client and use LookupClientProvider instead. ' +
11
+ 'See https://github.com/Totem/totem-sdk for setup instructions.');
12
+ this.name = 'LookupNodeNotImplementedError';
13
+ }
14
+ }
15
+ export class LookupNodeProvider {
16
+ getCoins(_query) {
17
+ throw new LookupNodeNotImplementedError();
18
+ }
19
+ getCoin(_coinId) {
20
+ throw new LookupNodeNotImplementedError();
21
+ }
22
+ getProof(_coinId) {
23
+ throw new LookupNodeNotImplementedError();
24
+ }
25
+ getTip() {
26
+ throw new LookupNodeNotImplementedError();
27
+ }
28
+ getToken(_tokenId) {
29
+ throw new LookupNodeNotImplementedError();
30
+ }
31
+ searchTokens(_query) {
32
+ throw new LookupNodeNotImplementedError();
33
+ }
34
+ getTokensByCreator(_address) {
35
+ throw new LookupNodeNotImplementedError();
36
+ }
37
+ broadcastTxPoW(_txpowHex) {
38
+ throw new LookupNodeNotImplementedError();
39
+ }
40
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * PureMinimaRpcProvider — thin wrapper over @totemsdk/pureminima-rpc.
3
+ */
4
+ import type { PureMinimaClient } from '@totemsdk/pureminima-rpc';
5
+ import type { ChainStateProvider, CoinsQuery, Coin, MMRProof, ChainTip, TokenInfo, TokenSearchQuery, BroadcastResult } from '../types.js';
6
+ export declare class PureMinimaRpcProvider implements ChainStateProvider {
7
+ private readonly client;
8
+ constructor(client: PureMinimaClient);
9
+ getCoins(query: CoinsQuery): Promise<Coin[]>;
10
+ getCoin(coinId: string): Promise<Coin | null>;
11
+ getProof(coinId: string): Promise<MMRProof>;
12
+ getTip(): Promise<ChainTip>;
13
+ getToken(tokenId: string): Promise<TokenInfo>;
14
+ searchTokens(query: TokenSearchQuery): Promise<TokenInfo[]>;
15
+ getTokensByCreator(address: string): Promise<TokenInfo[]>;
16
+ broadcastTxPoW(txpowHex: string): Promise<BroadcastResult>;
17
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * PureMinimaRpcProvider — thin wrapper over @totemsdk/pureminima-rpc.
3
+ */
4
+ export class PureMinimaRpcProvider {
5
+ constructor(client) {
6
+ this.client = client;
7
+ }
8
+ async getCoins(query) {
9
+ const result = await this.client.coins({
10
+ address: query.address,
11
+ tokenid: query.tokenId,
12
+ sendable: query.sendable,
13
+ relevant: query.relevant,
14
+ coinid: query.coinId,
15
+ megammr: query.megammr,
16
+ });
17
+ return result;
18
+ }
19
+ async getCoin(coinId) {
20
+ try {
21
+ const coins = await this.getCoins({ coinId });
22
+ return coins[0] ?? null;
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ async getProof(coinId) {
29
+ return this.client.mmrProof(coinId);
30
+ }
31
+ async getTip() {
32
+ return this.client.getTip();
33
+ }
34
+ async getToken(tokenId) {
35
+ const tokens = await this.client.tokens(tokenId);
36
+ if (!tokens || tokens.length === 0) {
37
+ throw new Error(`Token not found: ${tokenId}`);
38
+ }
39
+ return tokens[0];
40
+ }
41
+ async searchTokens(query) {
42
+ const all = await this.client.tokens();
43
+ let results = all;
44
+ if (query.name) {
45
+ const needle = query.name.toLowerCase();
46
+ results = results.filter((t) => {
47
+ const nameStr = JSON.stringify(t.name ?? '').toLowerCase();
48
+ return nameStr.includes(needle);
49
+ });
50
+ }
51
+ if (query.creatorAddress) {
52
+ const addr = query.creatorAddress;
53
+ results = results.filter((t) => {
54
+ const desc = JSON.stringify(t.description ?? '');
55
+ return desc.includes(addr);
56
+ });
57
+ }
58
+ if (query.offset)
59
+ results = results.slice(query.offset);
60
+ if (query.limit)
61
+ results = results.slice(0, query.limit);
62
+ return results;
63
+ }
64
+ async getTokensByCreator(address) {
65
+ return this.searchTokens({ creatorAddress: address });
66
+ }
67
+ async broadcastTxPoW(txpowHex) {
68
+ try {
69
+ const result = await this.client.txnMinePost(txpowHex);
70
+ return {
71
+ success: true,
72
+ txpowid: result?.txpowid,
73
+ message: 'broadcast via PureMinima txnminepost',
74
+ };
75
+ }
76
+ catch (e) {
77
+ return { success: false, message: String(e) };
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @totemsdk/chain-provider — shared types and ChainStateProvider interface
3
+ */
4
+ export interface CoinsQuery {
5
+ address?: string;
6
+ tokenId?: string;
7
+ sendable?: boolean;
8
+ relevant?: boolean;
9
+ coinId?: string;
10
+ megammr?: boolean;
11
+ }
12
+ export interface Coin {
13
+ coinid: string;
14
+ amount: string;
15
+ address: string;
16
+ miniaddress?: string;
17
+ tokenid: string;
18
+ token?: unknown;
19
+ storestate?: boolean;
20
+ state?: unknown[];
21
+ spent?: boolean;
22
+ mmrentry?: string;
23
+ created?: string;
24
+ }
25
+ export interface MMRProof {
26
+ coinid: string;
27
+ data: unknown;
28
+ }
29
+ export interface ChainTip {
30
+ block: number;
31
+ hash: string;
32
+ time?: string;
33
+ }
34
+ export interface TokenInfo {
35
+ tokenid: string;
36
+ name: Record<string, unknown>;
37
+ total?: string;
38
+ confirmed?: string;
39
+ sendable?: string;
40
+ coins?: number;
41
+ script?: string;
42
+ description?: unknown;
43
+ }
44
+ export interface TokenSearchQuery {
45
+ name?: string;
46
+ category?: string[];
47
+ creatorAddress?: string;
48
+ limit?: number;
49
+ offset?: number;
50
+ }
51
+ export interface BroadcastResult {
52
+ txpowid?: string;
53
+ success: boolean;
54
+ message?: string;
55
+ }
56
+ export interface ChainStateProvider {
57
+ getCoins(query: CoinsQuery): Promise<Coin[]>;
58
+ getCoin(coinId: string): Promise<Coin | null>;
59
+ getProof(coinId: string): Promise<MMRProof>;
60
+ getTip(): Promise<ChainTip>;
61
+ getToken(tokenId: string): Promise<TokenInfo>;
62
+ searchTokens(query: TokenSearchQuery): Promise<TokenInfo[]>;
63
+ getTokensByCreator(address: string): Promise<TokenInfo[]>;
64
+ broadcastTxPoW(txpowHex: string): Promise<BroadcastResult>;
65
+ }
package/dist/types.js ADDED
@@ -0,0 +1,4 @@
1
+ /**
2
+ * @totemsdk/chain-provider — shared types and ChainStateProvider interface
3
+ */
4
+ export {};
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@totemsdk/chain-provider",
3
+ "version": "0.1.0",
4
+ "description": "Unified ChainStateProvider interface for Totem SDK — Hosted, PureMinima, Composite",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "clean": "rm -rf dist",
17
+ "test": "jest --passWithNoTests"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "src"
22
+ ],
23
+ "dependencies": {
24
+ "@totemsdk/core": "workspace:*",
25
+ "@totemsdk/pureminima-rpc": "workspace:*"
26
+ },
27
+ "devDependencies": {
28
+ "@noble/hashes": "^1.3.0",
29
+ "@types/jest": "^29.0.0",
30
+ "@types/node": "^20.0.0",
31
+ "jest": "^29.0.0",
32
+ "ts-jest": "^29.0.0",
33
+ "typescript": "^5.0.0"
34
+ },
35
+ "engines": {
36
+ "node": ">=18.0.0"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "license": "MIT"
42
+ }
@@ -0,0 +1,116 @@
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 ADDED
@@ -0,0 +1,19 @@
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';
@@ -0,0 +1,72 @@
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
+ }
@@ -0,0 +1,129 @@
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
+ }
@@ -0,0 +1,55 @@
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
+ }
@@ -0,0 +1,95 @@
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 ADDED
@@ -0,0 +1,73 @@
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
+ }