@hsuite/smart-engines-cli 1.2.1 → 1.4.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/README.md +272 -58
- package/dist/_lib/attestation.d.ts +45 -0
- package/dist/_lib/attestation.d.ts.map +1 -0
- package/dist/_lib/attestation.js +76 -0
- package/dist/_lib/attestation.js.map +1 -0
- package/dist/_lib/baas.d.ts +147 -0
- package/dist/_lib/baas.d.ts.map +1 -0
- package/dist/_lib/baas.js +260 -0
- package/dist/_lib/baas.js.map +1 -0
- package/dist/_lib/cluster.d.ts +6 -6
- package/dist/_lib/cluster.js +6 -6
- package/dist/_lib/deploy-manifest.d.ts +69 -0
- package/dist/_lib/deploy-manifest.d.ts.map +1 -0
- package/dist/_lib/deploy-manifest.js +87 -0
- package/dist/_lib/deploy-manifest.js.map +1 -0
- package/dist/_lib/docker.d.ts +82 -0
- package/dist/_lib/docker.d.ts.map +1 -0
- package/dist/_lib/docker.js +164 -0
- package/dist/_lib/docker.js.map +1 -0
- package/dist/_lib/faucet-client.d.ts +64 -0
- package/dist/_lib/faucet-client.d.ts.map +1 -0
- package/dist/_lib/faucet-client.js +92 -0
- package/dist/_lib/faucet-client.js.map +1 -0
- package/dist/_lib/framework.d.ts +59 -0
- package/dist/_lib/framework.d.ts.map +1 -0
- package/dist/_lib/framework.js +149 -0
- package/dist/_lib/framework.js.map +1 -0
- package/dist/_lib/frontend-guard.d.ts +13 -0
- package/dist/_lib/frontend-guard.d.ts.map +1 -0
- package/dist/_lib/frontend-guard.js +41 -0
- package/dist/_lib/frontend-guard.js.map +1 -0
- package/dist/_lib/registry-fanout.d.ts +114 -0
- package/dist/_lib/registry-fanout.d.ts.map +1 -0
- package/dist/_lib/registry-fanout.js +435 -0
- package/dist/_lib/registry-fanout.js.map +1 -0
- package/dist/_lib/runtime-env.d.ts +126 -0
- package/dist/_lib/runtime-env.d.ts.map +1 -0
- package/dist/_lib/runtime-env.js +172 -0
- package/dist/_lib/runtime-env.js.map +1 -0
- package/dist/_lib/subscription-client.d.ts +129 -18
- package/dist/_lib/subscription-client.d.ts.map +1 -1
- package/dist/_lib/subscription-client.js +268 -40
- package/dist/_lib/subscription-client.js.map +1 -1
- package/dist/_lib/subscription-env.d.ts +35 -0
- package/dist/_lib/subscription-env.d.ts.map +1 -1
- package/dist/_lib/subscription-env.js +46 -2
- package/dist/_lib/subscription-env.js.map +1 -1
- package/dist/commands/app-lifecycle.d.ts +36 -0
- package/dist/commands/app-lifecycle.d.ts.map +1 -0
- package/dist/commands/app-lifecycle.js +145 -0
- package/dist/commands/app-lifecycle.js.map +1 -0
- package/dist/commands/ci.d.ts +45 -0
- package/dist/commands/ci.d.ts.map +1 -0
- package/dist/commands/ci.js +174 -0
- package/dist/commands/ci.js.map +1 -0
- package/dist/commands/deploy-manifest.d.ts +11 -0
- package/dist/commands/deploy-manifest.d.ts.map +1 -0
- package/dist/commands/deploy-manifest.js +17 -0
- package/dist/commands/deploy-manifest.js.map +1 -0
- package/dist/commands/deploy.d.ts +70 -11
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +263 -150
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/downgrade.js +1 -1
- package/dist/commands/downgrade.js.map +1 -1
- package/dist/commands/faucet.d.ts +78 -0
- package/dist/commands/faucet.d.ts.map +1 -0
- package/dist/commands/faucet.js +184 -0
- package/dist/commands/faucet.js.map +1 -0
- package/dist/commands/governance.js +1 -1
- package/dist/commands/governance.js.map +1 -1
- package/dist/commands/hist-balance.d.ts +1 -1
- package/dist/commands/hist-balance.js +1 -1
- package/dist/commands/init.js +1 -1
- package/dist/commands/list.d.ts +22 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +81 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/personhood.js +2 -2
- package/dist/commands/personhood.js.map +1 -1
- package/dist/commands/redeploy.d.ts +62 -0
- package/dist/commands/redeploy.d.ts.map +1 -0
- package/dist/commands/redeploy.js +310 -0
- package/dist/commands/redeploy.js.map +1 -0
- package/dist/commands/status.d.ts +51 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +138 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/subscribe.d.ts +94 -17
- package/dist/commands/subscribe.d.ts.map +1 -1
- package/dist/commands/subscribe.js +450 -81
- package/dist/commands/subscribe.js.map +1 -1
- package/dist/commands/transactions.d.ts +60 -0
- package/dist/commands/transactions.d.ts.map +1 -0
- package/dist/commands/transactions.js +101 -0
- package/dist/commands/transactions.js.map +1 -0
- package/dist/commands/update.d.ts +40 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +114 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/commands/verify.d.ts +10 -19
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +14 -23
- package/dist/commands/verify.js.map +1 -1
- package/dist/index.d.ts +12 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -3
- package/dist/index.js.map +1 -1
- package/package.json +11 -4
|
@@ -6,160 +6,511 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.subscribeCommand = void 0;
|
|
7
7
|
exports.runSubscribe = runSubscribe;
|
|
8
8
|
/**
|
|
9
|
-
* `hsuite subscribe` —
|
|
10
|
-
*
|
|
9
|
+
* `hsuite subscribe` — Onboard a smart-app the CANONICAL, MINT-FIRST way:
|
|
10
|
+
* create the smart-app's DKG ENTITY first, mint the builder subscription NFT
|
|
11
|
+
* ON THAT ENTITY's id, and land it on the dev wallet — so `1 smart-app =
|
|
12
|
+
* 1 DKG entity = 1 NFT`, ACTIVE from mint.
|
|
11
13
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
14
|
+
* ## Why this creates the entity (the mint-first re-architecture)
|
|
15
|
+
*
|
|
16
|
+
* `subscribe` used to mint the subscription NFT on a throwaway random
|
|
17
|
+
* `app-${uuid}` that was NEVER a DKG entity, while `hsuite deploy` separately
|
|
18
|
+
* minted a FRESH entity (+ a SECOND subscription) at `init` time. Paid id ≠
|
|
19
|
+
* deployed id → the deployed app stayed PENDING, redeploys forked, and two
|
|
20
|
+
* NFTs were minted. Now `subscribe` creates the durable DKG entity up front and
|
|
21
|
+
* uses its `entityId` as the subscription appId end-to-end; `deploy` reuses
|
|
22
|
+
* that same id (it no longer creates anything). Output:
|
|
23
|
+
* `SUBSCRIPTION_APP_ID == DEPLOYED_APP_ID == DKG entityId`, ACTIVE, ONE NFT.
|
|
24
|
+
*
|
|
25
|
+
* Full flow:
|
|
26
|
+
* 1. Read XRPL_ADDRESS + XRPL_SEED from env (written by `hsuite init`) and the
|
|
27
|
+
* validator URL.
|
|
28
|
+
* 2. Web3-auth against the validator (challenge → sign → bearer). The
|
|
29
|
+
* subscription + create routes are session-guarded. (Auth routes have NO
|
|
30
|
+
* /api prefix.)
|
|
31
|
+
* 3. POST /api/v3/smart-apps (createApp) — run the per-entity DKG ceremony
|
|
32
|
+
* to derive the canonical `entityId` AND create the (paid, pending-deposit)
|
|
33
|
+
* subscription in the same in-process flow. The response carries
|
|
34
|
+
* `{ appId: entityId, depositInstructions, subscriptionId }`. THIS replaces
|
|
35
|
+
* the old random-id + standalone `subscription/request`. From here the
|
|
36
|
+
* `entityId` is THE appId for everything downstream.
|
|
37
|
+
* 4. Ensure deposit instructions: the createApp response normally carries them.
|
|
38
|
+
* If a node deferred deposit-account creation, fall back to the idempotent
|
|
39
|
+
* POST /api/v3/subscription/request { appId: entityId, … } (same retry/503
|
|
40
|
+
* semantics, returns the SAME subscription's deposit instructions — no
|
|
41
|
+
* second subscription, no second deposit wallet).
|
|
42
|
+
* 5. Render the deposit instructions.
|
|
43
|
+
* 6. AUTO-PAY the deposit from the dev wallet over the XRPL testnet (HST
|
|
44
|
+
* Payment with a SendMax covering the 0.25% transfer fee). `--no-autopay`
|
|
45
|
+
* skips this and prints instructions for manual (XUMM) payment.
|
|
46
|
+
* 7. Poll GET /api/v3/subscription/status/:entityId until the deposit is
|
|
47
|
+
* confirmed and ready to mint (status in { deposit_confirmed, pending_mint,
|
|
48
|
+
* minting, active }; the real confirmed status is `deposit_confirmed`).
|
|
49
|
+
* 8. POST /api/v3/subscription/mint/:entityId — activates the subscription +
|
|
50
|
+
* flips the SmartApp doc PENDING_SUBSCRIPTION → ACTIVE (via `activateApp`,
|
|
51
|
+
* gated on the deposit-backed NFT mint) and kicks off the async NFT mint.
|
|
52
|
+
* 9. Poll /status until `subscriptionNftSerial` is set OR `chainNfts.xrpl.nftId`
|
|
53
|
+
* is present (the NFT mints asynchronously via the mint subscriber).
|
|
54
|
+
* 10. Accept the brokered NFT sell offer so it lands on the dev wallet.
|
|
55
|
+
* 11. Verify account_nfts, then write SUBSCRIPTION_APP_ID + APP_ID +
|
|
56
|
+
* SUBSCRIPTION_NFT_SERIAL (= entityId) to the env file.
|
|
22
57
|
*/
|
|
23
58
|
const commander_1 = require("commander");
|
|
24
59
|
const chalk_1 = __importDefault(require("chalk"));
|
|
25
60
|
const ora_1 = __importDefault(require("ora"));
|
|
26
|
-
const crypto_1 = require("crypto");
|
|
27
61
|
const env_1 = require("../_lib/env");
|
|
28
62
|
const subscription_client_1 = require("../_lib/subscription-client");
|
|
29
63
|
const subscription_env_1 = require("../_lib/subscription-env");
|
|
30
|
-
|
|
64
|
+
/** Public XRPL testnet websocket endpoint (matches `hsuite init` + faucet). */
|
|
65
|
+
const XRPL_TESTNET_WSS = 'wss://s.altnet.rippletest.net:51233';
|
|
66
|
+
/**
|
|
67
|
+
* Multiplier applied to the deposit amount for the Payment `SendMax` so the
|
|
68
|
+
* 0.25% HST transfer fee is covered (0.25% → ×1.0025; we use ×1.004 for a small
|
|
69
|
+
* safety margin against rounding).
|
|
70
|
+
*/
|
|
71
|
+
const HST_TRANSFER_FEE_MULTIPLIER = 1.004;
|
|
72
|
+
// Every subscription is paid + NFT-backed (the validator's createSubscription
|
|
73
|
+
// rejects any unknown tier on the public path). The four-tier ladder is
|
|
74
|
+
// `builder → growth → scale → enterprise`; `builder` ($49/mo) is the entry tier.
|
|
75
|
+
const VALID_TIERS = ['builder', 'growth', 'scale', 'enterprise'];
|
|
31
76
|
const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
77
|
+
function defaultXrplClientFactory(wss) {
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
79
|
+
const { Client } = require('xrpl');
|
|
80
|
+
return new Client(wss);
|
|
81
|
+
}
|
|
82
|
+
function defaultWalletSignFactory(seed) {
|
|
83
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
84
|
+
const { Wallet } = require('xrpl');
|
|
85
|
+
const wallet = Wallet.fromSeed(seed);
|
|
86
|
+
return (tx) => wallet.sign(tx).tx_blob;
|
|
87
|
+
}
|
|
32
88
|
/**
|
|
33
89
|
* Pure orchestration body — exported for unit tests.
|
|
34
90
|
*
|
|
35
|
-
* Returns the appId + NFT serial (
|
|
36
|
-
* Throws on every error path; the command-level action wraps
|
|
37
|
-
* exits the process.
|
|
91
|
+
* Returns the appId + NFT serial (once minted + accepted) so callers can
|
|
92
|
+
* persist them. Throws on every hard-error path; the command-level action wraps
|
|
93
|
+
* this and exits the process.
|
|
38
94
|
*/
|
|
39
95
|
async function runSubscribe(opts) {
|
|
40
96
|
const env = (0, env_1.readEnv)(opts.env);
|
|
41
|
-
const { address } = (0, subscription_env_1.requireXrplCreds)(env);
|
|
97
|
+
const { address, seed } = (0, subscription_env_1.requireXrplCreds)(env);
|
|
42
98
|
const tier = opts.tier;
|
|
43
99
|
if (!VALID_TIERS.includes(tier)) {
|
|
44
100
|
throw new Error(`unknown tier "${opts.tier}". Valid: ${VALID_TIERS.join(', ')}`);
|
|
45
101
|
}
|
|
46
|
-
const selectedNetworks = opts.networks
|
|
47
|
-
.split(',')
|
|
48
|
-
.map((s) => s.trim())
|
|
49
|
-
.filter(Boolean);
|
|
50
|
-
if (selectedNetworks.length === 0) {
|
|
51
|
-
throw new Error('At least one network required (e.g. --networks xrpl)');
|
|
52
|
-
}
|
|
53
102
|
const baseUrl = (0, subscription_env_1.requireValidatorUrl)(env, { gateway: opts.gateway });
|
|
54
|
-
const client = (opts.clientFactory ??
|
|
103
|
+
const client = (opts.clientFactory ??
|
|
104
|
+
((b) => new subscription_client_1.SubscriptionClient({ baseUrl: b, auth: { chain: 'xrpl', address, seed } })))(baseUrl);
|
|
105
|
+
// The subscription routes are session-guarded: obtain a validator bearer
|
|
106
|
+
// session by signing a challenge before any call.
|
|
107
|
+
await client.authenticate();
|
|
55
108
|
const sleep = opts.sleep ?? defaultSleep;
|
|
56
|
-
const appId = opts.appId ??
|
|
57
|
-
env.get('SUBSCRIPTION_APP_ID') ??
|
|
58
|
-
`app-${(0, crypto_1.randomUUID)().slice(0, 8)}`;
|
|
59
109
|
const appName = opts.appName ?? env.get('APP_NAME') ?? 'Smart App Showcase';
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
110
|
+
// A DNS-safe name is required by the create DTO (the entityId is derived by
|
|
111
|
+
// the DKG ceremony, not from this name; the name is the display label).
|
|
112
|
+
const dnsName = toDnsSafeName(appName);
|
|
113
|
+
// Step 1: CREATE THE DKG ENTITY (mint-first). POST /api/v3/smart-apps runs
|
|
114
|
+
// the per-entity DKG ceremony → canonical `entityId`, AND creates the (paid,
|
|
115
|
+
// pending-deposit) subscription on that same id in one in-process flow. The
|
|
116
|
+
// `entityId` it returns is THE appId for the subscription + deposit + mint +
|
|
117
|
+
// every later deploy. This REPLACES the old throwaway `app-${uuid}` + a
|
|
118
|
+
// standalone `subscription/request`.
|
|
119
|
+
//
|
|
120
|
+
// An explicit --app-id / a previously-written SUBSCRIPTION_APP_ID short-
|
|
121
|
+
// circuits create (re-running `subscribe` to finish paying/minting an
|
|
122
|
+
// already-created entity), so a resumed run does not fork a second entity.
|
|
123
|
+
const preexistingAppId = opts.appId ?? env.get('SUBSCRIPTION_APP_ID');
|
|
124
|
+
let appId;
|
|
125
|
+
let created;
|
|
126
|
+
if (preexistingAppId) {
|
|
127
|
+
appId = preexistingAppId;
|
|
71
128
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
129
|
+
else {
|
|
130
|
+
try {
|
|
131
|
+
created = await client.createApp({
|
|
132
|
+
name: appName,
|
|
133
|
+
services: ['database'],
|
|
134
|
+
// `config.appId` is a placeholder — SmartAppService.create overwrites it
|
|
135
|
+
// with the DKG `entityId`. acceptedLoginChains must be non-empty.
|
|
136
|
+
config: {
|
|
137
|
+
appId: dnsName,
|
|
138
|
+
acceptedLoginChains: ['xrpl'],
|
|
139
|
+
acceptedPaymentTokens: [],
|
|
140
|
+
},
|
|
141
|
+
tier,
|
|
142
|
+
networks: ['testnet'],
|
|
143
|
+
// validatorRules omitted (optional) — the factory synthesizes the
|
|
144
|
+
// canonical security-first default for the standard deployment path.
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
throw new Error(`smart-app entity creation failed: ${err.message}`);
|
|
149
|
+
}
|
|
150
|
+
if (!created.appId) {
|
|
151
|
+
throw new Error('create returned no appId (DKG entityId) — server contract violation');
|
|
152
|
+
}
|
|
153
|
+
appId = created.appId;
|
|
77
154
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
155
|
+
// Step 2: obtain the HST deposit instructions for THIS entityId. createApp
|
|
156
|
+
// normally returns them (the subscription was created alongside the entity).
|
|
157
|
+
// If a node deferred deposit-account creation, fall back to the idempotent
|
|
158
|
+
// POST /api/v3/subscription/request { appId: entityId } — it carries the
|
|
159
|
+
// retry/503 semantics and returns the SAME subscription's deposit
|
|
160
|
+
// instructions (no second subscription, no second deposit wallet).
|
|
161
|
+
let requested;
|
|
162
|
+
let deposit = created?.depositInstructions;
|
|
163
|
+
let subscriptionId = created?.subscriptionId;
|
|
164
|
+
if (!deposit) {
|
|
165
|
+
try {
|
|
166
|
+
requested = await client.request({
|
|
167
|
+
appId,
|
|
168
|
+
developerAccountId: address,
|
|
169
|
+
chain: 'xrpl',
|
|
170
|
+
appName,
|
|
171
|
+
selectedTier: tier,
|
|
172
|
+
selectedNetworks: ['xrpl'],
|
|
173
|
+
});
|
|
82
174
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
throw new Error('paid tier returned no depositInstructions — server contract violation');
|
|
175
|
+
catch (err) {
|
|
176
|
+
throw new Error(`subscription request failed: ${err.message}`);
|
|
86
177
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
178
|
+
deposit = requested.depositInstructions;
|
|
179
|
+
subscriptionId = subscriptionId ?? requested.subscriptionId;
|
|
180
|
+
}
|
|
181
|
+
if (!deposit) {
|
|
182
|
+
throw new Error('no depositInstructions for the created entity — server contract violation');
|
|
183
|
+
}
|
|
184
|
+
renderDepositInstructions(deposit, tier, appId);
|
|
185
|
+
// Step 3: auto-pay the deposit from the dev wallet (unless --no-autopay).
|
|
186
|
+
if (opts.noAutopay) {
|
|
187
|
+
renderManualPaymentHint(deposit);
|
|
188
|
+
return {
|
|
189
|
+
appId,
|
|
190
|
+
subscriptionId,
|
|
191
|
+
tier,
|
|
192
|
+
status: requested?.status ?? 'pending_deposit',
|
|
193
|
+
depositPaid: false,
|
|
194
|
+
nftAccepted: false,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
await payDeposit(opts, address, seed, deposit);
|
|
198
|
+
// Step 3: poll /status until the deposit is confirmed and ready to mint.
|
|
199
|
+
await pollStatus(client, appId, sleep, opts, (s) => isReadyToMint(s.status));
|
|
200
|
+
// Step 4: POST /mint — kicks off the async NFT mint. Tolerate a non-2xx here
|
|
201
|
+
// (the server may still be finalizing); the status poll below is the source of
|
|
202
|
+
// truth for whether the NFT actually minted.
|
|
203
|
+
try {
|
|
90
204
|
const mintResp = await client.mint(appId);
|
|
91
205
|
if (!mintResp.success) {
|
|
92
|
-
|
|
206
|
+
console.log(chalk_1.default.yellow(' mint reported success=false — continuing to poll status'));
|
|
93
207
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
console.log(chalk_1.default.yellow(` mint HTTP error (tolerated, will poll status): ${err.message}`));
|
|
211
|
+
}
|
|
212
|
+
// Step 5: poll until the NFT serial is set OR the minted NFT coordinates
|
|
213
|
+
// appear on /status (the NFT mints asynchronously via the mint subscriber).
|
|
214
|
+
// The 2-of-3 issuer MPC mint + offer creation take ~15-25s, so poll ~24×5s
|
|
215
|
+
// (~120s) before giving up. Bounded by attempt count (not wall-clock) so the
|
|
216
|
+
// injected-`sleep` unit tests stay fast.
|
|
217
|
+
const status = await pollStatus(client, appId, sleep, opts, (s) => Boolean(s.subscriptionNftSerial) ||
|
|
218
|
+
Boolean(s.chainNfts?.xrpl?.nftId) ||
|
|
219
|
+
(s.status ?? '').toLowerCase() === 'active', opts.nftPollAttempts ?? 24);
|
|
220
|
+
// Step 6: accept the brokered sell offer so the NFT lands on the dev wallet.
|
|
221
|
+
const nftAccepted = await acceptSubscriptionNft(opts, address, seed, sleep, status);
|
|
97
222
|
return {
|
|
98
223
|
appId,
|
|
99
|
-
subscriptionId: status.subscriptionId,
|
|
224
|
+
subscriptionId: status.subscriptionId ?? subscriptionId,
|
|
100
225
|
nftSerial: status.subscriptionNftSerial,
|
|
101
226
|
tier,
|
|
102
227
|
status: status.status,
|
|
228
|
+
depositPaid: true,
|
|
229
|
+
nftAccepted,
|
|
103
230
|
};
|
|
104
231
|
}
|
|
105
|
-
|
|
232
|
+
/**
|
|
233
|
+
* Coerce a display name into the DNS-safe shape the validator's create DTO
|
|
234
|
+
* requires (lowercase alphanumeric + hyphens, no leading/trailing hyphen, ≤63
|
|
235
|
+
* chars). The DKG `entityId` is derived by the ceremony — this only sanitises
|
|
236
|
+
* the placeholder `config.appId` + name field so a human-readable APP_NAME like
|
|
237
|
+
* "Smart App Showcase" doesn't 400 the create.
|
|
238
|
+
*/
|
|
239
|
+
function toDnsSafeName(name) {
|
|
240
|
+
const slug = name
|
|
241
|
+
.toLowerCase()
|
|
242
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
243
|
+
.replace(/^-+|-+$/g, '')
|
|
244
|
+
.slice(0, 63)
|
|
245
|
+
.replace(/-+$/g, '');
|
|
246
|
+
return slug.length > 0 ? slug : 'smart-app';
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Pay the HST deposit from the dev wallet over the XRPL testnet.
|
|
250
|
+
*
|
|
251
|
+
* The HST issuer is resolved from the deposit account's HST trust line: query
|
|
252
|
+
* `account_lines` on the deposit `walletAddress`, find the line with currency
|
|
253
|
+
* 'HST', and use its `account` (the trust-line peer) as the issuer. We then
|
|
254
|
+
* submit a `Payment` of `amount` whole HST with a `SendMax` of
|
|
255
|
+
* `amount × 1.004` to cover the 0.25% HST transfer fee.
|
|
256
|
+
*
|
|
257
|
+
* Requires tesSUCCESS. Always disconnects.
|
|
258
|
+
*/
|
|
259
|
+
async function payDeposit(opts, address, seed, deposit) {
|
|
260
|
+
if (deposit.chain !== 'xrpl') {
|
|
261
|
+
throw new Error(`auto-pay only supports XRPL deposits (got chain=${deposit.chain}); ` +
|
|
262
|
+
'use --no-autopay and pay manually');
|
|
263
|
+
}
|
|
264
|
+
const walletSign = (opts.walletSignFactory ?? defaultWalletSignFactory)(seed);
|
|
265
|
+
const client = (opts.xrplClientFactory ?? defaultXrplClientFactory)(XRPL_TESTNET_WSS);
|
|
266
|
+
await client.connect();
|
|
267
|
+
try {
|
|
268
|
+
const issuer = await resolveHstIssuer(client, deposit.walletAddress);
|
|
269
|
+
const value = deposit.amount;
|
|
270
|
+
const sendMax = (Number(value) * HST_TRANSFER_FEE_MULTIPLIER).toFixed(6);
|
|
271
|
+
const tx = {
|
|
272
|
+
TransactionType: 'Payment',
|
|
273
|
+
Account: address,
|
|
274
|
+
Destination: deposit.walletAddress,
|
|
275
|
+
Amount: { currency: 'HST', issuer, value },
|
|
276
|
+
SendMax: { currency: 'HST', issuer, value: sendMax },
|
|
277
|
+
};
|
|
278
|
+
const prepared = await client.autofill(tx);
|
|
279
|
+
const txBlob = walletSign(prepared);
|
|
280
|
+
const result = await client.submitAndWait(txBlob);
|
|
281
|
+
const code = result.result?.meta?.TransactionResult;
|
|
282
|
+
if (code !== 'tesSUCCESS') {
|
|
283
|
+
throw new Error(`deposit Payment failed: ${code ?? 'unknown result'}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
finally {
|
|
287
|
+
await client.disconnect();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Resolve the HST issuer from the deposit account's trust lines.
|
|
292
|
+
*
|
|
293
|
+
* The deposit account holds an HST trust line whose peer (`account`) is the HST
|
|
294
|
+
* issuer. We query `account_lines` on the deposit wallet and return the peer of
|
|
295
|
+
* the line whose `currency` is 'HST'.
|
|
296
|
+
*/
|
|
297
|
+
async function resolveHstIssuer(client, depositWallet) {
|
|
298
|
+
const res = await client.request({ command: 'account_lines', account: depositWallet });
|
|
299
|
+
const lines = res.result?.lines ?? [];
|
|
300
|
+
const hst = lines.find((l) => l?.currency === 'HST');
|
|
301
|
+
if (!hst?.account) {
|
|
302
|
+
throw new Error(`could not resolve HST issuer: deposit account ${depositWallet} has no HST trust line`);
|
|
303
|
+
}
|
|
304
|
+
return hst.account;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Ready-to-mint check.
|
|
308
|
+
*
|
|
309
|
+
* The server's ready-to-mint state is `deposit_confirmed`
|
|
310
|
+
* (`activateSubscription` refuses to mint unless status === DEPOSIT_CONFIRMED).
|
|
311
|
+
* We treat any of {deposit_confirmed, pending_mint, minting, active} as ready so
|
|
312
|
+
* the flow is robust to a server that already advanced past the gate by the time
|
|
313
|
+
* we poll. NOTE: `pending_mint` is NOT the confirmed status — `deposit_confirmed`
|
|
314
|
+
* is.
|
|
315
|
+
*/
|
|
316
|
+
function isReadyToMint(status) {
|
|
106
317
|
if (!status)
|
|
107
318
|
return false;
|
|
108
|
-
// SubscriptionStatus enum values from libs/multi-chain-core. Accept both
|
|
109
|
-
// canonical `pending_mint` and an already-active state (server may skip
|
|
110
|
-
// straight to ACTIVE if it's already minted by the time we poll).
|
|
111
319
|
const s = status.toLowerCase();
|
|
112
|
-
return s === '
|
|
320
|
+
return (s === 'deposit_confirmed' ||
|
|
321
|
+
s === 'pending_mint' ||
|
|
322
|
+
s === 'minting' ||
|
|
323
|
+
s === 'active');
|
|
113
324
|
}
|
|
114
|
-
async function pollStatus(client, appId, sleep, opts, done) {
|
|
325
|
+
async function pollStatus(client, appId, sleep, opts, done, maxAttempts) {
|
|
115
326
|
const interval = opts.pollIntervalMs ?? 5000;
|
|
116
327
|
const timeout = opts.pollTimeoutMs ?? 5 * 60 * 1000;
|
|
117
328
|
const deadline = Date.now() + timeout;
|
|
118
329
|
// First tick immediately so tests that prime a single response don't have
|
|
119
|
-
// to wait the full 5s.
|
|
330
|
+
// to wait the full 5s. Stops on the `done` predicate, an attempt cap, or the
|
|
331
|
+
// wall-clock deadline — whichever comes first.
|
|
332
|
+
let attempt = 0;
|
|
120
333
|
while (true) {
|
|
121
334
|
const status = await client.status(appId);
|
|
122
335
|
if (done(status))
|
|
123
336
|
return status;
|
|
337
|
+
attempt += 1;
|
|
338
|
+
if (maxAttempts !== undefined && attempt >= maxAttempts) {
|
|
339
|
+
throw new Error(`timed out waiting for subscription status for ${appId} after ${attempt} polls`);
|
|
340
|
+
}
|
|
124
341
|
if (Date.now() > deadline) {
|
|
125
342
|
throw new Error(`timed out waiting for subscription status for ${appId} after ${timeout}ms`);
|
|
126
343
|
}
|
|
127
344
|
await sleep(interval);
|
|
128
345
|
}
|
|
129
346
|
}
|
|
130
|
-
|
|
131
|
-
|
|
347
|
+
/**
|
|
348
|
+
* Accept the subscription NFT sell offer on the dev wallet.
|
|
349
|
+
*
|
|
350
|
+
* The mint creates an XRPL `NFTokenCreateOffer` (SELL, Amount '0',
|
|
351
|
+
* Destination = dev wallet) owned by the issuer — so the NFT does NOT
|
|
352
|
+
* auto-land. The dev wallet must submit `NFTokenAcceptOffer`.
|
|
353
|
+
*
|
|
354
|
+
* Strategy:
|
|
355
|
+
* a. Poll the dev wallet's `account_nfts` first — if the NFT is already there
|
|
356
|
+
* (already accepted on a prior run, or auto-delivered), we're done.
|
|
357
|
+
* b. Else, if `chainNfts.xrpl.nftId` is known, query `nft_sell_offers` for
|
|
358
|
+
* that NFTokenID and accept the offer whose `destination` is the dev wallet
|
|
359
|
+
* (else the first offer). Exact + issuer-free.
|
|
360
|
+
* c. Else fall back to the issuer-side scan: enumerate the issuer's
|
|
361
|
+
* `account_objects` (type `nft_offer`) and accept the sell offer whose
|
|
362
|
+
* `Destination` is the dev wallet. Issuer comes from
|
|
363
|
+
* `chainNfts.xrpl.issuerAddress`, with --nft-issuer / SUBSCRIPTION_NFT_ISSUER
|
|
364
|
+
* as an optional override.
|
|
365
|
+
* d. With no nftId, no issuer, and no NFT yet, return false (the mint still
|
|
366
|
+
* succeeded; only the on-wallet delivery is deferred).
|
|
367
|
+
*
|
|
368
|
+
* Retries up to 10× (≈50s at the 5s default) because the brokered sell offer
|
|
369
|
+
* needs a few ledgers to appear on-ledger after the 2-of-3 issuer MPC mint —
|
|
370
|
+
* re-querying BOTH `nft_sell_offers{nft_id}` and the issuer-side
|
|
371
|
+
* `account_objects` fallback on each attempt. Returns true when the NFT is
|
|
372
|
+
* confirmed on the dev wallet.
|
|
373
|
+
*/
|
|
374
|
+
async function acceptSubscriptionNft(opts, address, seed, sleep, status) {
|
|
375
|
+
const xrplNft = status?.chainNfts?.xrpl;
|
|
376
|
+
const nftId = xrplNft?.nftId;
|
|
377
|
+
// Explicit override wins; otherwise use the issuer the status now exposes.
|
|
378
|
+
const issuer = opts.nftIssuer ?? xrplNft?.issuerAddress;
|
|
379
|
+
const client = (opts.xrplClientFactory ?? defaultXrplClientFactory)(XRPL_TESTNET_WSS);
|
|
380
|
+
const walletSign = (opts.walletSignFactory ?? defaultWalletSignFactory)(seed);
|
|
381
|
+
const attempts = opts.acceptAttempts ?? 10;
|
|
382
|
+
const retryMs = opts.acceptRetryMs ?? opts.pollIntervalMs ?? 5000;
|
|
383
|
+
await client.connect();
|
|
384
|
+
try {
|
|
385
|
+
for (let i = 0; i < attempts; i++) {
|
|
386
|
+
// (a) Already on the wallet?
|
|
387
|
+
if (await walletHasAnyNft(client, address))
|
|
388
|
+
return true;
|
|
389
|
+
// (b)/(c) Resolve the sell-offer index: prefer the NFTokenID-keyed lookup
|
|
390
|
+
// (exact, issuer-free), fall back to the issuer-side account_objects scan.
|
|
391
|
+
let offerIndex;
|
|
392
|
+
if (nftId) {
|
|
393
|
+
offerIndex = await findSellOfferIndexByNftId(client, nftId, address);
|
|
394
|
+
}
|
|
395
|
+
if (!offerIndex && issuer) {
|
|
396
|
+
offerIndex = await findSellOfferIndex(client, issuer, address);
|
|
397
|
+
}
|
|
398
|
+
if (offerIndex) {
|
|
399
|
+
const tx = {
|
|
400
|
+
TransactionType: 'NFTokenAcceptOffer',
|
|
401
|
+
Account: address,
|
|
402
|
+
NFTokenSellOffer: offerIndex,
|
|
403
|
+
};
|
|
404
|
+
const prepared = await client.autofill(tx);
|
|
405
|
+
const txBlob = walletSign(prepared);
|
|
406
|
+
const result = await client.submitAndWait(txBlob);
|
|
407
|
+
const code = result.result?.meta?.TransactionResult;
|
|
408
|
+
if (code !== 'tesSUCCESS') {
|
|
409
|
+
throw new Error(`NFTokenAcceptOffer failed: ${code ?? 'unknown result'}`);
|
|
410
|
+
}
|
|
411
|
+
// Confirm the NFT is now on the wallet.
|
|
412
|
+
if (await walletHasAnyNft(client, address))
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
if (i < attempts - 1)
|
|
416
|
+
await sleep(retryMs);
|
|
417
|
+
}
|
|
418
|
+
// Final check after the loop.
|
|
419
|
+
return await walletHasAnyNft(client, address);
|
|
420
|
+
}
|
|
421
|
+
finally {
|
|
422
|
+
await client.disconnect();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
/** True if the account holds at least one NFT. */
|
|
426
|
+
async function walletHasAnyNft(client, account) {
|
|
427
|
+
const res = await client.request({ command: 'account_nfts', account });
|
|
428
|
+
const nfts = res.result?.account_nfts;
|
|
429
|
+
return Array.isArray(nfts) && nfts.length > 0;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Find the ledger index of the issuer's NFT SELL offer brokered to `recipient`.
|
|
433
|
+
*
|
|
434
|
+
* Enumerates the issuer's `account_objects` of type `nft_offer` and returns the
|
|
435
|
+
* `index` of the offer whose `Destination` matches the recipient (else the first
|
|
436
|
+
* offer). (The mint sets `Destination = dev wallet` + `Flags = tfSellNFToken`.)
|
|
437
|
+
*/
|
|
438
|
+
async function findSellOfferIndex(client, issuer, recipient) {
|
|
439
|
+
const res = await client.request({
|
|
440
|
+
command: 'account_objects',
|
|
441
|
+
account: issuer,
|
|
442
|
+
type: 'nft_offer',
|
|
443
|
+
});
|
|
444
|
+
const objects = res.result?.account_objects ?? [];
|
|
445
|
+
const match = objects.find((o) => o?.Destination === recipient) ?? objects[0];
|
|
446
|
+
return match?.index;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Find the ledger index of the SELL offer for a specific `NFTokenID` brokered to
|
|
450
|
+
* `recipient`.
|
|
451
|
+
*
|
|
452
|
+
* Queries `nft_sell_offers` keyed by the NFTokenID — the exact, issuer-free path
|
|
453
|
+
* the validator's `chainNfts.xrpl.nftId` enables. Returns the `nft_offer_index`
|
|
454
|
+
* of the offer whose `destination` matches the dev wallet (else the first
|
|
455
|
+
* offer). Returns undefined when the offer isn't on-ledger yet.
|
|
456
|
+
*/
|
|
457
|
+
async function findSellOfferIndexByNftId(client, nftId, recipient) {
|
|
458
|
+
let res;
|
|
459
|
+
try {
|
|
460
|
+
res = await client.request({ command: 'nft_sell_offers', nft_id: nftId });
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
// `objectNotFound` is returned while no sell offer exists yet — treat as
|
|
464
|
+
// "not on-ledger yet" so the retry loop can fall back / wait.
|
|
465
|
+
return undefined;
|
|
466
|
+
}
|
|
467
|
+
const offers = res.result?.offers ?? [];
|
|
468
|
+
const match = offers.find((o) => o?.destination === recipient) ?? offers[0];
|
|
469
|
+
return match?.nft_offer_index;
|
|
470
|
+
}
|
|
471
|
+
function renderDepositInstructions(d, tier, appId) {
|
|
472
|
+
console.log(chalk_1.default.bold.green(`\n Subscription requested: ${appId}`));
|
|
132
473
|
console.log(chalk_1.default.bold.yellow('\n Deposit required to mint your subscription NFT:'));
|
|
133
474
|
console.log(chalk_1.default.gray(` tier: ${tier}`));
|
|
134
475
|
console.log(chalk_1.default.gray(` chain: ${d.chain}`));
|
|
135
|
-
console.log(chalk_1.default.gray(` amount: ${d.amount} (token ${d.tokenId})`));
|
|
476
|
+
console.log(chalk_1.default.gray(` amount: ${d.amount} HST (token ${d.tokenId})`));
|
|
136
477
|
console.log(chalk_1.default.gray(` to: ${d.walletAddress}\n`));
|
|
137
|
-
|
|
478
|
+
}
|
|
479
|
+
function renderManualPaymentHint(d) {
|
|
480
|
+
const xummDeeplink = buildXummDeeplink(d);
|
|
481
|
+
console.log(chalk_1.default.cyan(' --no-autopay: pay the deposit manually, then re-run `hsuite subscribe`.'));
|
|
482
|
+
console.log(chalk_1.default.cyan(' XUMM deeplink:'));
|
|
138
483
|
console.log(chalk_1.default.cyan(` ${xummDeeplink}\n`));
|
|
139
|
-
console.log(chalk_1.default.gray(' Waiting for deposit confirmation...\n'));
|
|
140
484
|
}
|
|
141
485
|
function buildXummDeeplink(d) {
|
|
142
486
|
if (d.chain !== 'xrpl') {
|
|
143
487
|
return `(no deeplink: chain=${d.chain})`;
|
|
144
488
|
}
|
|
145
|
-
// Minimal Xumm deeplink shape; full IssuedCurrency requires JSON payload
|
|
146
|
-
// pushed through https://xumm.app/sign — for the CLI we surface the raw
|
|
147
|
-
//
|
|
489
|
+
// Minimal Xumm deeplink shape; full IssuedCurrency requires a JSON payload
|
|
490
|
+
// pushed through https://xumm.app/sign — for the CLI we surface the raw tx
|
|
491
|
+
// params so the user can paste them into their wallet.
|
|
148
492
|
return `xumm://xrpl/send?to=${encodeURIComponent(d.walletAddress)}&amount=${encodeURIComponent(d.amount)}&token=${encodeURIComponent(d.tokenId)}`;
|
|
149
493
|
}
|
|
150
494
|
exports.subscribeCommand = new commander_1.Command('subscribe')
|
|
151
|
-
.description('
|
|
495
|
+
.description('Subscribe a smart-app: create DKG entity → auto-pay deposit → mint → accept subscription NFT (mint-first)')
|
|
152
496
|
.option('--env <file>', 'Env file with XRPL creds', '.env.local')
|
|
153
497
|
.option('--gateway <url>', 'Validator/gateway URL (defaults to VALIDATOR_URL in env)')
|
|
154
|
-
.option('--tier <tier>', 'Subscription tier (
|
|
155
|
-
.option('--
|
|
498
|
+
.option('--tier <tier>', 'Subscription tier (builder|growth|scale|enterprise)', 'builder')
|
|
499
|
+
.option('--app-id <id>', 'Resume an already-created entity by its appId (defaults to SUBSCRIPTION_APP_ID env). When unset, subscribe CREATES the DKG entity and uses its entityId.')
|
|
156
500
|
.option('--app-name <name>', 'Smart-app display name')
|
|
157
|
-
.option('--
|
|
501
|
+
.option('--no-autopay', 'Skip auto-paying the HST deposit; print instructions for manual (XUMM) payment')
|
|
502
|
+
.option('--nft-issuer <address>', 'Optional override for the XRPL subscription-NFT issuer. No longer required: the /status response now exposes chainNfts.xrpl (nftId + issuerAddress) and the offer is resolved from it automatically. Defaults to SUBSCRIPTION_NFT_ISSUER env.')
|
|
158
503
|
.action(async (opts) => {
|
|
159
504
|
console.log(chalk_1.default.bold.cyan('\n hsuite subscribe\n'));
|
|
160
505
|
console.log(chalk_1.default.gray(` env: ${opts.env}`));
|
|
161
|
-
console.log(chalk_1.default.gray(` tier: ${opts.tier}`));
|
|
162
|
-
|
|
506
|
+
console.log(chalk_1.default.gray(` tier: ${opts.tier}\n`));
|
|
507
|
+
// Resolve the NFT issuer from env when not passed on the CLI.
|
|
508
|
+
if (!opts.nftIssuer) {
|
|
509
|
+
const env = (0, env_1.readEnv)(opts.env);
|
|
510
|
+
const fromEnv = env.get('SUBSCRIPTION_NFT_ISSUER');
|
|
511
|
+
if (fromEnv)
|
|
512
|
+
opts.nftIssuer = fromEnv;
|
|
513
|
+
}
|
|
163
514
|
const spinner = (0, ora_1.default)(`requesting ${opts.tier} subscription...`).start();
|
|
164
515
|
let result;
|
|
165
516
|
try {
|
|
@@ -169,6 +520,17 @@ exports.subscribeCommand = new commander_1.Command('subscribe')
|
|
|
169
520
|
spinner.fail(err.message);
|
|
170
521
|
process.exit(1);
|
|
171
522
|
}
|
|
523
|
+
if (!result.depositPaid) {
|
|
524
|
+
spinner.info(`subscription requested (appId=${result.appId}) — deposit not paid (--no-autopay)`);
|
|
525
|
+
const updates = {
|
|
526
|
+
SUBSCRIPTION_APP_ID: result.appId,
|
|
527
|
+
APP_ID: result.appId,
|
|
528
|
+
};
|
|
529
|
+
(0, env_1.upsertEnv)(opts.env, updates);
|
|
530
|
+
console.log(chalk_1.default.gray(`\n wrote SUBSCRIPTION_APP_ID to ${opts.env}`));
|
|
531
|
+
console.log(chalk_1.default.yellow('\n Pay the deposit, then re-run `hsuite subscribe` to mint + accept the NFT.\n'));
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
172
534
|
spinner.succeed(`subscription active (appId=${result.appId})`);
|
|
173
535
|
if (result.nftSerial !== undefined) {
|
|
174
536
|
console.log(chalk_1.default.gray(` NFT serial: ${result.nftSerial}`));
|
|
@@ -176,6 +538,13 @@ exports.subscribeCommand = new commander_1.Command('subscribe')
|
|
|
176
538
|
if (result.subscriptionId) {
|
|
177
539
|
console.log(chalk_1.default.gray(` subscriptionId: ${result.subscriptionId}`));
|
|
178
540
|
}
|
|
541
|
+
if (result.nftAccepted) {
|
|
542
|
+
console.log(chalk_1.default.gray(' NFT accepted + confirmed on the dev wallet'));
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
console.log(chalk_1.default.yellow(' NFT not yet on the dev wallet (sell offer may not be on-ledger yet). ' +
|
|
546
|
+
'Re-run `hsuite subscribe` to retry accepting it.'));
|
|
547
|
+
}
|
|
179
548
|
const updates = {
|
|
180
549
|
SUBSCRIPTION_APP_ID: result.appId,
|
|
181
550
|
APP_ID: result.appId,
|