@subly_fi/pay 0.7.3 → 0.8.1

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/dist/deposit.js CHANGED
@@ -1886,6 +1886,18 @@ async function assertWithdrawalPreview(input) {
1886
1886
  }
1887
1887
  }
1888
1888
 
1889
+ // ../../src/lib/canonical-json.ts
1890
+ import { createHash as createHash4 } from "node:crypto";
1891
+ function canonicalJson2(value) {
1892
+ return stableStringify(value);
1893
+ }
1894
+ function sha256HexOf(data) {
1895
+ return createHash4("sha256").update(data, "utf8").digest("hex");
1896
+ }
1897
+ function canonicalJsonHash(value) {
1898
+ return sha256HexOf(canonicalJson2(value));
1899
+ }
1900
+
1889
1901
  // ../../src/client/lookup-tables.ts
1890
1902
  import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
1891
1903
  import { address, getCompiledTransactionMessageDecoder as getCompiledTransactionMessageDecoder2 } from "@solana/kit";
@@ -1941,6 +1953,11 @@ var VaultFlowClientError = class extends Error {
1941
1953
  code;
1942
1954
  errorDetails;
1943
1955
  };
1956
+ function vaultOperationKind(intentId) {
1957
+ if (/^dep_[0-9a-f]{32}$/.test(intentId)) return "deposit";
1958
+ if (/^wdr_[0-9a-f]{32}$/.test(intentId)) return "withdrawal";
1959
+ throw new VaultFlowClientError("read", "intentId must be the original dep_ or wdr_ ID followed by 32 lowercase hexadecimal characters");
1960
+ }
1944
1961
  var VaultFlowClient = class {
1945
1962
  vault;
1946
1963
  rpc;
@@ -2043,9 +2060,36 @@ var VaultFlowClient = class {
2043
2060
  ...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
2044
2061
  }
2045
2062
  );
2063
+ this.assertPreparedWithdrawal(prepared, input);
2064
+ await input.onPrepared?.(prepared);
2065
+ return this.submitPreparedWithdrawal(prepared, input);
2066
+ }
2067
+ /** Reconcile or submit the original intent; never prepare a replacement. */
2068
+ async resumeWithdrawal(prepared, input) {
2069
+ this.assertPreparedWithdrawal(prepared, input);
2070
+ const current = await this.getJson(
2071
+ `/v1/withdrawals/${encodeURIComponent(prepared.withdrawalId)}`
2072
+ );
2073
+ if (current.withdrawalId !== prepared.withdrawalId || current.wallet !== this.signer.walletAddress || current.vault !== this.vault.address || current.requestedWithdrawRawUsdc !== input.amountRawUsdc.toString() || current.purpose !== (input.purpose ?? "normal") || canonicalJsonHash(current.paymentBinding ?? null) !== canonicalJsonHash(input.payment ?? null) || current.serializedTransaction !== prepared.serializedTransaction || current.preparedMessageHash !== prepared.signingIntent.preparedMessageHash || current.destinationUsdcAta !== prepared.destinationUsdcAta) {
2074
+ throw new VaultFlowClientError("read", "Saved withdrawal differs from the original operation; refusing to resume");
2075
+ }
2076
+ if (current.status === "prepared") {
2077
+ return this.submitPreparedWithdrawal(prepared, input);
2078
+ }
2079
+ if (!["submitted", "confirmed", "failed", "failed_not_submitted", "expired", "quarantined"].includes(current.status)) {
2080
+ throw new VaultFlowClientError("read", "Relayer returned an unknown withdrawal status");
2081
+ }
2082
+ return this.withdrawalOutcome(prepared, current);
2083
+ }
2084
+ assertPreparedWithdrawal(prepared, input) {
2046
2085
  if (prepared.signingIntent?.wallet !== this.signer.walletAddress || prepared.signingIntent.vault !== this.vault.address || prepared.requestedWithdrawRawUsdc !== input.amountRawUsdc.toString() || prepared.purpose !== (input.purpose ?? "normal") || input.purpose === "yield_realize" && prepared.signingIntent.allowFullExit) {
2047
2086
  throw new VaultFlowClientError("prepare", "Prepared withdrawal differs from the requested operation");
2048
2087
  }
2088
+ if (typeof prepared.withdrawalId !== "string" || prepared.withdrawalId.length === 0) {
2089
+ throw new VaultFlowClientError("prepare", "Prepared withdrawal has no withdrawal ID");
2090
+ }
2091
+ }
2092
+ async submitPreparedWithdrawal(prepared, input) {
2049
2093
  await assertWithdrawalPreview({
2050
2094
  rpc: this.rpc,
2051
2095
  serializedTransaction: prepared.serializedTransaction,
@@ -2059,6 +2103,7 @@ var VaultFlowClient = class {
2059
2103
  serializedTransaction: prepared.serializedTransaction,
2060
2104
  lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
2061
2105
  });
2106
+ input.onBeforeSubmit?.();
2062
2107
  let outcome = await this.postJson("submit", "/v1/withdrawals/submit", {
2063
2108
  withdrawalId: prepared.withdrawalId,
2064
2109
  serializedTransaction: signed.serializedTransaction,
@@ -2070,6 +2115,9 @@ var VaultFlowClient = class {
2070
2115
  outcome
2071
2116
  );
2072
2117
  }
2118
+ return this.withdrawalOutcome(prepared, outcome);
2119
+ }
2120
+ withdrawalOutcome(prepared, outcome) {
2073
2121
  return {
2074
2122
  withdrawalId: prepared.withdrawalId,
2075
2123
  status: outcome.status,
@@ -2080,6 +2128,40 @@ var VaultFlowClient = class {
2080
2128
  errorCode: outcome.errorCode ?? null
2081
2129
  };
2082
2130
  }
2131
+ /** Authenticated read/reconciliation only: never prepare, sign or submit a transaction. */
2132
+ async getOperationStatus(intentId) {
2133
+ const kind = vaultOperationKind(intentId);
2134
+ const raw = await this.getJson(`/v1/${kind === "deposit" ? "deposits" : "withdrawals"}/${intentId}?resubmit=false`);
2135
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
2136
+ throw new VaultFlowClientError("read", "Relayer returned an invalid operation status");
2137
+ }
2138
+ const record = raw;
2139
+ if (record[kind === "deposit" ? "depositId" : "withdrawalId"] !== intentId || record.wallet !== this.signer.walletAddress || record.vault !== this.vault.address) {
2140
+ throw new VaultFlowClientError("read", "Operation does not match the requested ID, current wallet or selected vault; use the original wallet, vault and relayer");
2141
+ }
2142
+ const requested = record[kind === "deposit" ? "amountRawUsdc" : "requestedWithdrawRawUsdc"];
2143
+ const actual = record[kind === "deposit" ? "actualDepositRawUsdc" : "actualWithdrawRawUsdc"];
2144
+ if (typeof record.status !== "string" || !["prepared", "submitted", "confirmed", "failed", "expired", "failed_not_submitted"].includes(record.status) || typeof requested !== "string" || !/^\d+$/.test(requested) || actual !== null && (typeof actual !== "string" || !/^\d+$/.test(actual)) || record.txSignature !== null && (typeof record.txSignature !== "string" || record.txSignature.length === 0) || record.errorCode !== null && typeof record.errorCode !== "string" || record.status === "confirmed" && (actual === null || record.txSignature === null)) {
2145
+ throw new VaultFlowClientError("read", "Relayer returned incomplete or invalid operation status fields");
2146
+ }
2147
+ const status = record.status;
2148
+ const nextAction = status === "confirmed" ? "done" : status === "submitted" || status === "prepared" ? "check_again" : "reconcile_with_operator";
2149
+ const message = status === "confirmed" ? `The original ${kind} is confirmed.` : status === "submitted" ? "The original transaction is still confirming. Check this same intent ID again; do not repeat the deposit or withdrawal." : status === "prepared" ? "The original intent is prepared. This status check does not submit it. Check the same ID again or ask the operator to reconcile it before starting another operation." : "The original operation ended without a confirmed result. Reconcile its intent ID and transaction with the operator before starting another operation.";
2150
+ return {
2151
+ intentId,
2152
+ kind,
2153
+ wallet: this.signer.walletAddress,
2154
+ vault: this.vault.address,
2155
+ status,
2156
+ requestedAmountRawUsdc: requested,
2157
+ actualAmountRawUsdc: actual,
2158
+ txSignature: record.txSignature,
2159
+ errorCode: record.errorCode,
2160
+ stillConfirming: status === "submitted",
2161
+ nextAction,
2162
+ message
2163
+ };
2164
+ }
2083
2165
  /**
2084
2166
  * Reads the yield budget. Syncs the relayer's ledger from chain first (so
2085
2167
  * yield accrued since the last sync shows up); the sync is best-effort and
@@ -2333,12 +2415,19 @@ function formatRawUsdc(raw) {
2333
2415
  return `${whole}.${fraction}`;
2334
2416
  }
2335
2417
 
2418
+ // ../../demo/cli-command.ts
2419
+ import { readFileSync as readFileSync3 } from "node:fs";
2420
+ var { version } = JSON.parse(
2421
+ readFileSync3(new URL("../package.json", import.meta.url), "utf8")
2422
+ );
2423
+ var PAY_COMMAND = `npx -y @subly_fi/pay@${version}`;
2424
+
2336
2425
  // ../../demo/deposit.ts
2337
2426
  var relayerBaseUrl = process.env.SUBLY_RELAYER_URL ?? process.env.SUBLY_FACILITATOR_URL ?? "https://api.demo.sublyfi.com";
2338
2427
  var amountRawUsdc = process.argv[2];
2339
2428
  if (amountRawUsdc === void 0 || !/^[1-9]\d*$/.test(amountRawUsdc)) {
2340
2429
  fail(
2341
- "Usage: pay deposit <amountRawUsdc> [apr_<approvalId>] (positive integer, e.g. 60000000 = 60 USDC)"
2430
+ `Usage: ${PAY_COMMAND} deposit <amountRawUsdc> [apr_<approvalId>] (positive integer, e.g. 60000000 = 60 USDC)`
2342
2431
  );
2343
2432
  }
2344
2433
  var approvalId = process.argv[3];
@@ -2381,7 +2470,7 @@ try {
2381
2470
  approvalId: details.approvalId ?? null,
2382
2471
  approveUrl: details.approveUrl ?? null,
2383
2472
  expiresAtMs: details.expiresAtMs ?? null,
2384
- message: `This deposit needs the owner's approval. Paste approveUrl to the user; once they approve (Face ID / wallet sign), retry: pay deposit ${amountRawUsdc} ${details.approvalId ?? "<approvalId>"}`
2473
+ message: `This deposit needs the owner's approval. Paste approveUrl to the user; once they approve (Face ID / wallet sign), retry: ${PAY_COMMAND} deposit ${amountRawUsdc} ${details.approvalId ?? "<approvalId>"}`
2385
2474
  },
2386
2475
  null,
2387
2476
  2
@@ -2396,7 +2485,7 @@ try {
2396
2485
  {
2397
2486
  ok: false,
2398
2487
  setupRequired: true,
2399
- message: `Deposits require a registered owner. Create a setup link (pay setup-link --initial-deposit ${amountRawUsdc}), paste the setupUrl to the user, wait for their Face ID, then deposit again \u2014 the first deposit is pre-approved.`
2488
+ message: `Deposits require a registered owner. Create a setup link (${PAY_COMMAND} setup-link --initial-deposit ${amountRawUsdc}), paste the setupUrl to the user, wait for their Face ID, then deposit again \u2014 the first deposit is pre-approved.`
2400
2489
  },
2401
2490
  null,
2402
2491
  2
@@ -2418,12 +2507,12 @@ if (submitted.txSignature !== null) {
2418
2507
  }
2419
2508
  if (submitted.status === "submitted") {
2420
2509
  fail(
2421
- `[deposit] broadcast but not yet confirmed \u2014 it may still land. Do NOT resubmit; check GET /v1/deposits/${submitted.depositId} (or the tx link above) first`
2510
+ `[deposit] broadcast but not yet confirmed \u2014 it may still land. Do NOT resubmit; run ${PAY_COMMAND} status ${submitted.depositId} with the same wallet, vault and relayer first`
2422
2511
  );
2423
2512
  }
2424
2513
  if (submitted.status !== "confirmed") {
2425
2514
  fail(
2426
- `[deposit] not confirmed (errorCode=${submitted.errorCode}); check GET /v1/deposits/${submitted.depositId} and the relayer logs`
2515
+ `[deposit] not confirmed (errorCode=${submitted.errorCode}); run ${PAY_COMMAND} status ${submitted.depositId} and ask the operator to reconcile it`
2427
2516
  );
2428
2517
  }
2429
2518
  console.log(