blue-js-sdk 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +446 -0
- package/LICENSE +21 -0
- package/README.md +75 -0
- package/ai-path/ADMIN-ELEVATION.md +116 -0
- package/ai-path/AI-MANIFESTO.md +185 -0
- package/ai-path/BREAKING.md +74 -0
- package/ai-path/CHECKLIST.md +619 -0
- package/ai-path/CONNECTION-STEPS.md +724 -0
- package/ai-path/DECISION-TREE.md +378 -0
- package/ai-path/DEPENDENCIES.md +459 -0
- package/ai-path/E2E-FLOW.md +1555 -0
- package/ai-path/FAILURES.md +403 -0
- package/ai-path/GUIDE.md +1217 -0
- package/ai-path/README.md +558 -0
- package/ai-path/SPLIT-TUNNEL.md +266 -0
- package/ai-path/cli.js +535 -0
- package/ai-path/connect.js +884 -0
- package/ai-path/discover.js +178 -0
- package/ai-path/environment.js +266 -0
- package/ai-path/errors.js +86 -0
- package/ai-path/examples/autonomous-agent.mjs +220 -0
- package/ai-path/examples/multi-region.mjs +174 -0
- package/ai-path/examples/one-shot.mjs +31 -0
- package/ai-path/index.js +60 -0
- package/ai-path/pricing.js +136 -0
- package/ai-path/recommend.js +413 -0
- package/ai-path/run-admin.vbs +25 -0
- package/ai-path/setup.js +291 -0
- package/ai-path/wallet.js +137 -0
- package/app-helpers.js +363 -0
- package/app-settings.js +95 -0
- package/app-types.js +267 -0
- package/audit.js +847 -0
- package/batch.js +293 -0
- package/bin/setup.js +376 -0
- package/chain/authz.js +109 -0
- package/chain/broadcast.js +472 -0
- package/chain/client.js +160 -0
- package/chain/fee-grants.js +305 -0
- package/chain/index.js +891 -0
- package/chain/lcd.js +313 -0
- package/chain/queries.js +547 -0
- package/chain/rpc.js +408 -0
- package/chain/wallet.js +141 -0
- package/cli/config.js +143 -0
- package/cli/index.js +463 -0
- package/cli/output.js +182 -0
- package/cli.js +491 -0
- package/client/index.js +251 -0
- package/client.js +271 -0
- package/config/index.js +255 -0
- package/connection/connect.js +849 -0
- package/connection/disconnect.js +180 -0
- package/connection/discovery.js +321 -0
- package/connection/index.js +76 -0
- package/connection/proxy.js +148 -0
- package/connection/resilience.js +428 -0
- package/connection/security.js +232 -0
- package/connection/state.js +369 -0
- package/connection/tunnel.js +691 -0
- package/consumer.js +132 -0
- package/cosmjs-setup.js +1884 -0
- package/defaults.js +366 -0
- package/disk-cache.js +107 -0
- package/dist/client.d.ts +108 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +400 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/errors/index.js +112 -0
- package/errors.js +218 -0
- package/examples/README.md +64 -0
- package/examples/connect-direct.mjs +106 -0
- package/examples/connect-plan.mjs +125 -0
- package/examples/error-handling.mjs +109 -0
- package/examples/query-nodes.mjs +94 -0
- package/examples/wallet-basics.mjs +61 -0
- package/generated/amino/amino.ts +9 -0
- package/generated/cosmos/base/v1beta1/coin.ts +365 -0
- package/generated/cosmos_proto/cosmos.ts +323 -0
- package/generated/gogoproto/gogo.ts +9 -0
- package/generated/google/protobuf/descriptor.ts +7601 -0
- package/generated/google/protobuf/duration.ts +208 -0
- package/generated/google/protobuf/timestamp.ts +238 -0
- package/generated/sentinel/lease/v1/events.ts +924 -0
- package/generated/sentinel/lease/v1/lease.ts +292 -0
- package/generated/sentinel/lease/v1/msg.ts +949 -0
- package/generated/sentinel/lease/v1/params.ts +164 -0
- package/generated/sentinel/node/v3/events.ts +881 -0
- package/generated/sentinel/node/v3/msg.ts +1002 -0
- package/generated/sentinel/node/v3/node.ts +263 -0
- package/generated/sentinel/node/v3/params.ts +183 -0
- package/generated/sentinel/plan/v3/events.ts +675 -0
- package/generated/sentinel/plan/v3/msg.ts +1191 -0
- package/generated/sentinel/plan/v3/plan.ts +283 -0
- package/generated/sentinel/provider/v2/events.ts +171 -0
- package/generated/sentinel/provider/v2/msg.ts +480 -0
- package/generated/sentinel/provider/v2/params.ts +131 -0
- package/generated/sentinel/provider/v2/provider.ts +246 -0
- package/generated/sentinel/session/v3/events.ts +480 -0
- package/generated/sentinel/session/v3/msg.ts +616 -0
- package/generated/sentinel/session/v3/params.ts +260 -0
- package/generated/sentinel/session/v3/proof.ts +180 -0
- package/generated/sentinel/session/v3/session.ts +384 -0
- package/generated/sentinel/subscription/v3/events.ts +1181 -0
- package/generated/sentinel/subscription/v3/msg.ts +1305 -0
- package/generated/sentinel/subscription/v3/params.ts +167 -0
- package/generated/sentinel/subscription/v3/subscription.ts +315 -0
- package/generated/sentinel/types/v1/bandwidth.ts +124 -0
- package/generated/sentinel/types/v1/price.ts +149 -0
- package/generated/sentinel/types/v1/renewal.ts +87 -0
- package/generated/sentinel/types/v1/status.ts +54 -0
- package/generated/typeRegistry.ts +27 -0
- package/index.js +486 -0
- package/node-connect.js +3015 -0
- package/operator.js +134 -0
- package/package.json +113 -0
- package/plan-operations.js +199 -0
- package/preflight.js +352 -0
- package/pricing/index.js +262 -0
- package/proto/amino/amino.proto +84 -0
- package/proto/cosmos/base/v1beta1/coin.proto +61 -0
- package/proto/cosmos_proto/cosmos.proto +112 -0
- package/proto/gogoproto/gogo.proto +145 -0
- package/proto/google/api/annotations.proto +31 -0
- package/proto/google/api/http.proto +370 -0
- package/proto/google/protobuf/any.proto +106 -0
- package/proto/google/protobuf/duration.proto +115 -0
- package/proto/google/protobuf/timestamp.proto +145 -0
- package/proto/sentinel/lease/v1/events.proto +52 -0
- package/proto/sentinel/lease/v1/genesis.proto +15 -0
- package/proto/sentinel/lease/v1/lease.proto +25 -0
- package/proto/sentinel/lease/v1/msg.proto +62 -0
- package/proto/sentinel/lease/v1/params.proto +17 -0
- package/proto/sentinel/node/v3/events.proto +50 -0
- package/proto/sentinel/node/v3/genesis.proto +15 -0
- package/proto/sentinel/node/v3/msg.proto +63 -0
- package/proto/sentinel/node/v3/node.proto +27 -0
- package/proto/sentinel/node/v3/params.proto +21 -0
- package/proto/sentinel/node/v3/querier.proto +63 -0
- package/proto/sentinel/plan/v3/events.proto +41 -0
- package/proto/sentinel/plan/v3/genesis.proto +21 -0
- package/proto/sentinel/plan/v3/msg.proto +83 -0
- package/proto/sentinel/plan/v3/plan.proto +32 -0
- package/proto/sentinel/plan/v3/querier.proto +53 -0
- package/proto/sentinel/provider/v2/events.proto +16 -0
- package/proto/sentinel/provider/v2/genesis.proto +15 -0
- package/proto/sentinel/provider/v2/msg.proto +35 -0
- package/proto/sentinel/provider/v2/params.proto +17 -0
- package/proto/sentinel/provider/v2/provider.proto +24 -0
- package/proto/sentinel/provider/v3/genesis.proto +15 -0
- package/proto/sentinel/provider/v3/params.proto +13 -0
- package/proto/sentinel/session/v3/events.proto +30 -0
- package/proto/sentinel/session/v3/genesis.proto +15 -0
- package/proto/sentinel/session/v3/msg.proto +50 -0
- package/proto/sentinel/session/v3/params.proto +25 -0
- package/proto/sentinel/session/v3/proof.proto +25 -0
- package/proto/sentinel/session/v3/querier.proto +100 -0
- package/proto/sentinel/session/v3/session.proto +50 -0
- package/proto/sentinel/subscription/v2/allocation.proto +21 -0
- package/proto/sentinel/subscription/v2/payout.proto +22 -0
- package/proto/sentinel/subscription/v3/events.proto +65 -0
- package/proto/sentinel/subscription/v3/genesis.proto +17 -0
- package/proto/sentinel/subscription/v3/msg.proto +83 -0
- package/proto/sentinel/subscription/v3/params.proto +21 -0
- package/proto/sentinel/subscription/v3/subscription.proto +33 -0
- package/proto/sentinel/types/v1/bandwidth.proto +19 -0
- package/proto/sentinel/types/v1/price.proto +21 -0
- package/proto/sentinel/types/v1/renewal.proto +21 -0
- package/proto/sentinel/types/v1/status.proto +16 -0
- package/protocol/encoding.js +341 -0
- package/protocol/events.js +361 -0
- package/protocol/handshake.js +297 -0
- package/protocol/index.js +15 -0
- package/protocol/messages.js +346 -0
- package/protocol/plans.js +199 -0
- package/protocol/v2ray.js +268 -0
- package/protocol/v3.js +723 -0
- package/protocol/wireguard.js +125 -0
- package/security/index.js +132 -0
- package/session-manager.js +329 -0
- package/session-tracker.js +80 -0
- package/setup.js +376 -0
- package/speedtest/index.js +528 -0
- package/speedtest.js +567 -0
- package/src/client.ts +502 -0
- package/src/index.ts +20 -0
- package/state/index.js +347 -0
- package/state.js +516 -0
- package/test-all-chain-ops.js +493 -0
- package/test-all-logic.js +199 -0
- package/test-all-msg-types.js +292 -0
- package/test-every-connection.js +208 -0
- package/test-feegrant-connect.js +98 -0
- package/test-logic.js +148 -0
- package/test-mainnet.js +176 -0
- package/test-plan-lifecycle.js +335 -0
- package/tls-trust.js +132 -0
- package/tsconfig.build.json +20 -0
- package/tsconfig.json +34 -0
- package/types/chain.d.ts +746 -0
- package/types/connection.d.ts +425 -0
- package/types/errors.d.ts +174 -0
- package/types/index.d.ts +1380 -0
- package/types/nodes.d.ts +187 -0
- package/types/pricing.d.ts +156 -0
- package/types/protocol.d.ts +332 -0
- package/types/session.d.ts +236 -0
- package/types/settings.d.ts +192 -0
- package/v3protocol.js +1053 -0
- package/wallet/index.js +153 -0
- package/wireguard.js +307 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* FULL PLAN LIFECYCLE TEST — Live Mainnet
|
|
4
|
+
*
|
|
5
|
+
* Tests the entire commercial/white-label dVPN flow:
|
|
6
|
+
* 1. Register as provider
|
|
7
|
+
* 2. Create a plan
|
|
8
|
+
* 3. Find a good node and link it to the plan
|
|
9
|
+
* 4. Generate a new user wallet
|
|
10
|
+
* 5. Transfer P2P to the user wallet
|
|
11
|
+
* 6. Subscribe the user to the plan
|
|
12
|
+
* 7. Issue fee grant to the user
|
|
13
|
+
* 8. Connect the user via the plan (gas-free)
|
|
14
|
+
* 9. Verify connection works
|
|
15
|
+
* 10. Disconnect and clean up
|
|
16
|
+
*
|
|
17
|
+
* Cost: ~5-10 P2P total (plan creation + session + transfer + gas)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import 'dotenv/config';
|
|
21
|
+
const operatorMnemonic = process.env.MNEMONIC;
|
|
22
|
+
if (!operatorMnemonic) { console.error('Set MNEMONIC in .env'); process.exit(1); }
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
createWallet, generateWallet, getBalance, formatP2P, sendTokens,
|
|
26
|
+
createClient, broadcast, broadcastWithFeeGrant,
|
|
27
|
+
fetchAllNodes, queryPlanNodes, discoverPlans,
|
|
28
|
+
subscribeToPlan, hasActiveSubscription,
|
|
29
|
+
connectViaPlan, connectDirect, disconnect,
|
|
30
|
+
encodeMsgCreatePlan, encodeMsgUpdatePlanStatus, encodeMsgLinkNode,
|
|
31
|
+
encodeMsgRegisterProvider, encodeMsgStartLease,
|
|
32
|
+
buildFeeGrantMsg, queryFeeGrants,
|
|
33
|
+
sentToSentprov, extractId,
|
|
34
|
+
registerCleanupHandlers, nodeStatusV3, createNodeHttpsAgent,
|
|
35
|
+
DEFAULT_RPC, LCD_ENDPOINTS,
|
|
36
|
+
} from './index.js';
|
|
37
|
+
|
|
38
|
+
registerCleanupHandlers();
|
|
39
|
+
|
|
40
|
+
const R = { pass: 0, fail: 0, errors: [] };
|
|
41
|
+
async function t(name, fn) {
|
|
42
|
+
try {
|
|
43
|
+
const r = await fn();
|
|
44
|
+
if (r) { R.pass++; console.log(' ✓', name); return r; }
|
|
45
|
+
else { R.fail++; R.errors.push(name); console.log(' ✗', name, '→ falsy'); return null; }
|
|
46
|
+
} catch (e) {
|
|
47
|
+
R.fail++; R.errors.push(name + ': ' + e.message?.slice(0, 150));
|
|
48
|
+
console.log(' ✗', name, '→', e.message?.slice(0, 150));
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log('═══════════════════════════════════════');
|
|
54
|
+
console.log(' FULL PLAN LIFECYCLE TEST — Live Mainnet');
|
|
55
|
+
console.log('═══════════════════════════════════════\n');
|
|
56
|
+
|
|
57
|
+
// ─── Step 1: Operator wallet setup ──────────────────────────────────────────
|
|
58
|
+
console.log('═══ STEP 1: OPERATOR WALLET ═══');
|
|
59
|
+
const { wallet: opWallet, account: opAccount } = await createWallet(operatorMnemonic);
|
|
60
|
+
const opClient = await createClient(DEFAULT_RPC, opWallet);
|
|
61
|
+
const opBal = await getBalance(opClient, opAccount.address);
|
|
62
|
+
console.log(' Operator:', opAccount.address);
|
|
63
|
+
console.log(' Balance:', formatP2P(opBal.udvpn));
|
|
64
|
+
console.log(' Provider:', sentToSentprov(opAccount.address));
|
|
65
|
+
|
|
66
|
+
if (opBal.udvpn < 5_000_000) {
|
|
67
|
+
console.error(' Need at least 5 P2P to run this test');
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ─── Step 2: Register provider (if not already) ────────────────────────────
|
|
72
|
+
console.log('\n═══ STEP 2: REGISTER PROVIDER ═══');
|
|
73
|
+
const provAddr = sentToSentprov(opAccount.address);
|
|
74
|
+
await t('2.1 Register/update provider', async () => {
|
|
75
|
+
// Encoders return raw protobuf bytes — wrap in { typeUrl, value }
|
|
76
|
+
try {
|
|
77
|
+
const msg = {
|
|
78
|
+
typeUrl: '/sentinel.provider.v3.MsgRegisterProviderRequest',
|
|
79
|
+
value: { from: opAccount.address, name: 'SDK Test Provider', identity: '', website: '', description: 'Automated SDK lifecycle test' },
|
|
80
|
+
};
|
|
81
|
+
const result = await broadcast(opClient, opAccount.address, [msg]);
|
|
82
|
+
console.log(' Registered. TX:', result.transactionHash);
|
|
83
|
+
return true;
|
|
84
|
+
} catch (e) {
|
|
85
|
+
if (e.message?.includes('already registered') || e.message?.includes('duplicate')) {
|
|
86
|
+
console.log(' Already registered (OK)');
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
throw e;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ─── Step 3: Create a plan ──────────────────────────────────────────────────
|
|
94
|
+
console.log('\n═══ STEP 3: CREATE PLAN ═══');
|
|
95
|
+
let planId = null;
|
|
96
|
+
await t('3.1 Create plan (1GB, 30 days, 1 P2P)', async () => {
|
|
97
|
+
// encodeMsgCreatePlan expects: { from, bytes, duration, prices }
|
|
98
|
+
// bytes = total bandwidth in bytes string (1 GB = 1000000000)
|
|
99
|
+
// duration = { seconds: N } — plan validity
|
|
100
|
+
// prices = subscription price array
|
|
101
|
+
const planMsg = {
|
|
102
|
+
typeUrl: '/sentinel.plan.v3.MsgCreatePlanRequest',
|
|
103
|
+
value: {
|
|
104
|
+
from: provAddr,
|
|
105
|
+
bytes: '1000000000', // 1 GB
|
|
106
|
+
duration: { seconds: 30 * 24 * 3600 }, // 30 days
|
|
107
|
+
prices: [{ denom: 'udvpn', base_value: '0.000001000000000000', quote_value: '1000000' }],
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
const result = await broadcast(opClient, opAccount.address, [planMsg]);
|
|
111
|
+
planId = extractId(result, /plan/i, ['plan_id', 'id']);
|
|
112
|
+
if (!planId) {
|
|
113
|
+
// Try extracting from events differently
|
|
114
|
+
for (const ev of (result.events || [])) {
|
|
115
|
+
for (const attr of (ev.attributes || [])) {
|
|
116
|
+
if (attr.key === 'plan_id' || attr.key === 'id') {
|
|
117
|
+
planId = attr.value;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
// Try base64 decode
|
|
121
|
+
try {
|
|
122
|
+
const decoded = Buffer.from(attr.key, 'base64').toString('utf8');
|
|
123
|
+
if (decoded === 'plan_id' || decoded === 'id') {
|
|
124
|
+
planId = Buffer.from(attr.value, 'base64').toString('utf8');
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
} catch {}
|
|
128
|
+
}
|
|
129
|
+
if (planId) break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
console.log(' Plan created. ID:', planId, 'TX:', result.transactionHash);
|
|
133
|
+
return !!planId;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (!planId) {
|
|
137
|
+
console.error(' Plan creation failed — cannot continue');
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ─── Step 3b: Activate plan ─────────────────────────────────────────────────
|
|
142
|
+
await t('3.2 Activate plan', async () => {
|
|
143
|
+
// encodeMsgUpdatePlanStatus expects: { from, id, status }
|
|
144
|
+
const activateMsg = {
|
|
145
|
+
typeUrl: '/sentinel.plan.v3.MsgUpdatePlanStatusRequest',
|
|
146
|
+
value: { from: provAddr, id: parseInt(planId), status: 1 },
|
|
147
|
+
};
|
|
148
|
+
const result = await broadcast(opClient, opAccount.address, [activateMsg]);
|
|
149
|
+
console.log(' Plan activated. TX:', result.transactionHash);
|
|
150
|
+
return result.code === 0;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ─── Step 4: Find a good WG node and link it ───────────────────────────────
|
|
154
|
+
console.log('\n═══ STEP 4: LINK NODE TO PLAN ═══');
|
|
155
|
+
const allNodes = await fetchAllNodes();
|
|
156
|
+
let linkedNode = null;
|
|
157
|
+
|
|
158
|
+
// Find a WG node that responds
|
|
159
|
+
for (const n of allNodes.slice(0, 30)) {
|
|
160
|
+
try {
|
|
161
|
+
const url = 'https://' + (n.remote_addrs?.[0] || '');
|
|
162
|
+
if (!url || url === 'https://') continue;
|
|
163
|
+
const agent = createNodeHttpsAgent(n.address, 'tofu');
|
|
164
|
+
const status = await nodeStatusV3(url, agent);
|
|
165
|
+
if (status.type === 'wireguard' && (!status.address || status.address === n.address)) {
|
|
166
|
+
linkedNode = n;
|
|
167
|
+
console.log(' Found WG node:', n.address, '-', status.moniker);
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
} catch {}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!linkedNode) {
|
|
174
|
+
console.error(' No reachable WG node found — cannot link');
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
await t('4.1 Lease node (required before linking)', async () => {
|
|
179
|
+
try {
|
|
180
|
+
const gbPrice = linkedNode.gigabyte_prices?.find(p => p.denom === 'udvpn');
|
|
181
|
+
if (!gbPrice) throw new Error('Node has no udvpn pricing');
|
|
182
|
+
// encodeMsgStartLease expects: { from, nodeAddress, hours, maxPrice, renewalPricePolicy }
|
|
183
|
+
// maxPrice must EXACTLY match node's hourly_prices
|
|
184
|
+
const hrPrice = linkedNode.hourly_prices?.find(p => p.denom === 'udvpn') || gbPrice;
|
|
185
|
+
const leaseMsg = {
|
|
186
|
+
typeUrl: '/sentinel.lease.v1.MsgStartLeaseRequest',
|
|
187
|
+
value: {
|
|
188
|
+
from: provAddr,
|
|
189
|
+
nodeAddress: linkedNode.address,
|
|
190
|
+
hours: 1,
|
|
191
|
+
maxPrice: hrPrice, // must match node's price exactly
|
|
192
|
+
renewalPricePolicy: 7,
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
const result = await broadcast(opClient, opAccount.address, [leaseMsg]);
|
|
196
|
+
console.log(' Leased. TX:', result.transactionHash);
|
|
197
|
+
return result.code === 0;
|
|
198
|
+
} catch (e) {
|
|
199
|
+
if (e.message?.includes('already exists') || e.message?.includes('active lease')) {
|
|
200
|
+
console.log(' Active lease exists (OK)');
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
throw e;
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await t('4.2 Link node to plan', async () => {
|
|
208
|
+
try {
|
|
209
|
+
// encodeMsgLinkNode expects: { from, id, nodeAddress }
|
|
210
|
+
const linkMsg = {
|
|
211
|
+
typeUrl: '/sentinel.plan.v3.MsgLinkNodeRequest',
|
|
212
|
+
value: { from: provAddr, id: parseInt(planId), nodeAddress: linkedNode.address },
|
|
213
|
+
};
|
|
214
|
+
const result = await broadcast(opClient, opAccount.address, [linkMsg]);
|
|
215
|
+
console.log(' Linked. TX:', result.transactionHash);
|
|
216
|
+
return result.code === 0;
|
|
217
|
+
} catch (e) {
|
|
218
|
+
if (e.message?.includes('duplicate') || e.message?.includes('already')) {
|
|
219
|
+
console.log(' Already linked (OK)');
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
throw e;
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Verify plan has nodes
|
|
227
|
+
await t('4.3 queryPlanNodes returns linked node', async () => {
|
|
228
|
+
const { items } = await queryPlanNodes(planId);
|
|
229
|
+
console.log(' Plan nodes:', items.length);
|
|
230
|
+
return items.length > 0;
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// ─── Step 5: Generate user wallet + fund it ─────────────────────────────────
|
|
234
|
+
console.log('\n═══ STEP 5: CREATE USER WALLET ═══');
|
|
235
|
+
const { mnemonic: userMnemonic, account: userAccount } = await generateWallet();
|
|
236
|
+
console.log(' User address:', userAccount.address);
|
|
237
|
+
console.log(' User mnemonic: (generated, not printed for safety)');
|
|
238
|
+
|
|
239
|
+
await t('5.1 Transfer 2 P2P to user wallet', async () => {
|
|
240
|
+
const result = await sendTokens(opClient, opAccount.address, userAccount.address, '2000000', 'udvpn');
|
|
241
|
+
console.log(' TX:', result.transactionHash);
|
|
242
|
+
return result.code === 0;
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Wait for transfer to confirm
|
|
246
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
247
|
+
|
|
248
|
+
await t('5.2 User balance = 2 P2P', async () => {
|
|
249
|
+
const { wallet: uWallet } = await createWallet(userMnemonic);
|
|
250
|
+
const uClient = await createClient(DEFAULT_RPC, uWallet);
|
|
251
|
+
const uBal = await getBalance(uClient, userAccount.address);
|
|
252
|
+
console.log(' User balance:', formatP2P(uBal.udvpn));
|
|
253
|
+
return uBal.udvpn >= 1_000_000;
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// ─── Step 6: Subscribe user to plan ─────────────────────────────────────────
|
|
257
|
+
console.log('\n═══ STEP 6: SUBSCRIBE TO PLAN ═══');
|
|
258
|
+
await t('6.1 hasActiveSubscription = false before subscribe', async () => {
|
|
259
|
+
const sub = await hasActiveSubscription(userAccount.address, planId);
|
|
260
|
+
console.log(' Has subscription:', sub.has);
|
|
261
|
+
return !sub.has;
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
await t('6.2 subscribeToPlan', async () => {
|
|
265
|
+
const { wallet: uWallet } = await createWallet(userMnemonic);
|
|
266
|
+
const uClient = await createClient(DEFAULT_RPC, uWallet);
|
|
267
|
+
const result = await subscribeToPlan(uClient, userAccount.address, planId);
|
|
268
|
+
console.log(' Subscription ID:', result.subscriptionId, 'TX:', result.txHash);
|
|
269
|
+
return !!result.subscriptionId;
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
273
|
+
|
|
274
|
+
await t('6.3 hasActiveSubscription = true after subscribe', async () => {
|
|
275
|
+
const sub = await hasActiveSubscription(userAccount.address, planId);
|
|
276
|
+
console.log(' Has subscription:', sub.has);
|
|
277
|
+
return sub.has;
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// ─── Step 7: Issue fee grant ────────────────────────────────────────────────
|
|
281
|
+
console.log('\n═══ STEP 7: FEE GRANT ═══');
|
|
282
|
+
await t('7.1 Grant fee allowance to user', async () => {
|
|
283
|
+
const grantMsg = buildFeeGrantMsg(opAccount.address, userAccount.address, {
|
|
284
|
+
spendLimit: 5_000_000, // 5 P2P max
|
|
285
|
+
});
|
|
286
|
+
const result = await broadcast(opClient, opAccount.address, [grantMsg]);
|
|
287
|
+
console.log(' TX:', result.transactionHash);
|
|
288
|
+
return result.code === 0;
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
292
|
+
|
|
293
|
+
await t('7.2 queryFeeGrants shows grant', async () => {
|
|
294
|
+
const lcd = LCD_ENDPOINTS[0]?.url || LCD_ENDPOINTS[0];
|
|
295
|
+
const grants = await queryFeeGrants(lcd, userAccount.address);
|
|
296
|
+
console.log(' Grants received:', grants.length);
|
|
297
|
+
if (grants.length > 0) console.log(' Granter:', grants[0].granter);
|
|
298
|
+
return grants.length > 0;
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// ─── Step 8: Connect user via plan with fee grant ───────────────────────────
|
|
302
|
+
console.log('\n═══ STEP 8: CONNECT VIA PLAN (FEE GRANTED) ═══');
|
|
303
|
+
await t('8.1 connectViaPlan with feeGranter', async () => {
|
|
304
|
+
const result = await connectViaPlan({
|
|
305
|
+
mnemonic: userMnemonic,
|
|
306
|
+
planId: parseInt(planId),
|
|
307
|
+
nodeAddress: linkedNode.address,
|
|
308
|
+
feeGranter: opAccount.address,
|
|
309
|
+
fullTunnel: false,
|
|
310
|
+
dns: 'handshake',
|
|
311
|
+
v2rayExePath: process.env.V2RAY_PATH || undefined, // SDK auto-detects
|
|
312
|
+
onProgress: (step, detail) => console.log(' [' + step + ']', detail),
|
|
313
|
+
});
|
|
314
|
+
console.log(' Session:', result.sessionId, 'Type:', result.serviceType);
|
|
315
|
+
console.log(' Connected via plan with fee grant!');
|
|
316
|
+
|
|
317
|
+
await disconnect();
|
|
318
|
+
console.log(' Disconnected');
|
|
319
|
+
return result.serviceType === 'wireguard';
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// ─── RESULTS ────────────────────────────────────────────────────────────────
|
|
323
|
+
console.log('\n═══════════════════════════════════════');
|
|
324
|
+
console.log(' PLAN LIFECYCLE TEST COMPLETE');
|
|
325
|
+
console.log(' Plan ID:', planId);
|
|
326
|
+
console.log(' Linked node:', linkedNode?.address);
|
|
327
|
+
console.log(' User wallet:', userAccount?.address);
|
|
328
|
+
console.log('═══════════════════════════════════════');
|
|
329
|
+
console.log('RESULTS:', R.pass, 'passed,', R.fail, 'failed');
|
|
330
|
+
if (R.errors.length > 0) {
|
|
331
|
+
console.log('\nFAILURES:');
|
|
332
|
+
for (const e of R.errors) console.log(' ✗', e);
|
|
333
|
+
}
|
|
334
|
+
console.log('═══════════════════════════════════════');
|
|
335
|
+
process.exit(R.fail > 0 ? 1 : 0);
|
package/tls-trust.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TLS Trust-On-First-Use (TOFU) for Sentinel Nodes
|
|
3
|
+
*
|
|
4
|
+
* Sentinel nodes use self-signed certificates (no CA issues certs for ephemeral IP servers).
|
|
5
|
+
* TOFU model: save cert fingerprint on first connect, reject if it changes later.
|
|
6
|
+
* Same concept as SSH known_hosts.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* import { createNodeHttpsAgent } from './tls-trust.js';
|
|
10
|
+
* const agent = createNodeHttpsAgent('sentnode1abc...', 'tofu');
|
|
11
|
+
* const res = await axios.post(url, body, { httpsAgent: agent });
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import https from 'https';
|
|
15
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import os from 'os';
|
|
18
|
+
import { SecurityError, ErrorCodes } from './errors.js';
|
|
19
|
+
|
|
20
|
+
const KNOWN_NODES_DIR = path.join(os.homedir(), '.sentinel-sdk');
|
|
21
|
+
const KNOWN_NODES_PATH = path.join(KNOWN_NODES_DIR, 'known_nodes.json');
|
|
22
|
+
|
|
23
|
+
// In-memory cache to avoid file I/O on every request
|
|
24
|
+
let knownNodesCache = null;
|
|
25
|
+
|
|
26
|
+
function loadKnownNodes() {
|
|
27
|
+
if (knownNodesCache) return knownNodesCache;
|
|
28
|
+
try {
|
|
29
|
+
knownNodesCache = JSON.parse(readFileSync(KNOWN_NODES_PATH, 'utf8'));
|
|
30
|
+
} catch {
|
|
31
|
+
knownNodesCache = {};
|
|
32
|
+
}
|
|
33
|
+
return knownNodesCache;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function saveKnownNodes(nodes) {
|
|
37
|
+
knownNodesCache = nodes;
|
|
38
|
+
try {
|
|
39
|
+
if (!existsSync(KNOWN_NODES_DIR)) mkdirSync(KNOWN_NODES_DIR, { recursive: true, mode: 0o700 });
|
|
40
|
+
writeFileSync(KNOWN_NODES_PATH, JSON.stringify(nodes, null, 2), { mode: 0o600 });
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.warn('[sentinel-sdk] Failed to save known_nodes.json:', e.message);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create an HTTPS agent with TOFU certificate pinning for a specific node.
|
|
48
|
+
*
|
|
49
|
+
* Modes:
|
|
50
|
+
* - 'tofu' (default): Pin cert on first connection, reject if it changes
|
|
51
|
+
* - 'none': Accept any cert (current behavior, for testing only)
|
|
52
|
+
*
|
|
53
|
+
* @param {string} nodeAddress - sentnode1... address (used as lookup key)
|
|
54
|
+
* @param {'tofu'|'none'} mode - Trust mode
|
|
55
|
+
* @returns {https.Agent}
|
|
56
|
+
*/
|
|
57
|
+
export function createNodeHttpsAgent(nodeAddress, mode = 'tofu') {
|
|
58
|
+
if (mode === 'none') {
|
|
59
|
+
return new https.Agent({ rejectUnauthorized: false });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const known = loadKnownNodes();
|
|
63
|
+
|
|
64
|
+
return new https.Agent({
|
|
65
|
+
rejectUnauthorized: false,
|
|
66
|
+
checkServerIdentity: (_hostname, cert) => {
|
|
67
|
+
const fingerprint = cert.fingerprint256;
|
|
68
|
+
if (!fingerprint) return new Error('Certificate missing fingerprint — possible MITM or malformed cert');
|
|
69
|
+
|
|
70
|
+
const saved = known[nodeAddress];
|
|
71
|
+
|
|
72
|
+
if (saved && saved.fingerprint !== fingerprint) {
|
|
73
|
+
throw new SecurityError(
|
|
74
|
+
ErrorCodes.TLS_CERT_CHANGED,
|
|
75
|
+
`TLS certificate CHANGED for ${nodeAddress}. ` +
|
|
76
|
+
`Expected: ${saved.fingerprint.substring(0, 20)}... ` +
|
|
77
|
+
`Got: ${fingerprint.substring(0, 20)}... ` +
|
|
78
|
+
`This could indicate a man-in-the-middle attack. ` +
|
|
79
|
+
`If the node legitimately rotated its certificate, call clearKnownNode('${nodeAddress}') or delete ~/.sentinel-sdk/known_nodes.json`,
|
|
80
|
+
{ nodeAddress, expected: saved.fingerprint, got: fingerprint, firstSeen: saved.firstSeen },
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!saved) {
|
|
85
|
+
known[nodeAddress] = {
|
|
86
|
+
fingerprint,
|
|
87
|
+
firstSeen: new Date().toISOString(),
|
|
88
|
+
lastSeen: new Date().toISOString(),
|
|
89
|
+
};
|
|
90
|
+
saveKnownNodes(known);
|
|
91
|
+
} else {
|
|
92
|
+
saved.lastSeen = new Date().toISOString();
|
|
93
|
+
saveKnownNodes(known);
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Clear a specific node's stored certificate fingerprint.
|
|
101
|
+
* Call after a node legitimately rotates its TLS cert.
|
|
102
|
+
*/
|
|
103
|
+
export function clearKnownNode(nodeAddress) {
|
|
104
|
+
const known = loadKnownNodes();
|
|
105
|
+
delete known[nodeAddress];
|
|
106
|
+
saveKnownNodes(known);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Clear all stored node certificate fingerprints.
|
|
111
|
+
*/
|
|
112
|
+
export function clearAllKnownNodes() {
|
|
113
|
+
saveKnownNodes({});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get stored certificate info for a node (null if not known).
|
|
118
|
+
*/
|
|
119
|
+
export function getKnownNode(nodeAddress) {
|
|
120
|
+
const known = loadKnownNodes();
|
|
121
|
+
return known[nodeAddress] || null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Secure agent for LCD/RPC public endpoints (CA-validated).
|
|
126
|
+
* These endpoints have valid CA-signed certificates — no reason to skip verification.
|
|
127
|
+
* TOFU is only for node-direct connections (self-signed certs).
|
|
128
|
+
*/
|
|
129
|
+
export const publicEndpointAgent = new https.Agent({ rejectUnauthorized: true });
|
|
130
|
+
|
|
131
|
+
/** @deprecated Use publicEndpointAgent. Kept for backward compatibility. */
|
|
132
|
+
export const insecureAgent = publicEndpointAgent;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "node16",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"declarationMap": true,
|
|
13
|
+
"sourceMap": true,
|
|
14
|
+
"outDir": "./dist",
|
|
15
|
+
"rootDir": "./src",
|
|
16
|
+
"lib": ["ES2022", "DOM"]
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*.ts"],
|
|
19
|
+
"exclude": ["node_modules"]
|
|
20
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"allowJs": true,
|
|
7
|
+
"checkJs": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"declarationDir": "./types",
|
|
16
|
+
"typeRoots": ["./types", "./node_modules/@types"],
|
|
17
|
+
"paths": {
|
|
18
|
+
"sentinel-dvpn-sdk": ["./types/index.d.ts"]
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"include": [
|
|
22
|
+
"*.js",
|
|
23
|
+
"chain/**/*.js",
|
|
24
|
+
"connection/**/*.js",
|
|
25
|
+
"protocol/**/*.js",
|
|
26
|
+
"types/**/*.d.ts"
|
|
27
|
+
],
|
|
28
|
+
"exclude": [
|
|
29
|
+
"node_modules",
|
|
30
|
+
"test",
|
|
31
|
+
"examples",
|
|
32
|
+
"bin"
|
|
33
|
+
]
|
|
34
|
+
}
|