@subly_fi/pay 0.7.3 → 0.8.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 +19 -13
- package/dist/budget.js +84 -2
- package/dist/cli.js +2 -1
- package/dist/deposit.js +84 -2
- package/dist/mcp-server.js +1548 -1238
- package/dist/pay.js +1381 -1117
- package/dist/setup-link.js +84 -2
- package/dist/status.js +2359 -0
- package/dist/withdraw.js +86 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
CLI and stdio MCP client for paying compatible x402 APIs with Kamino USDC vault yield on Solana. MIT licensed. Works with your own [Subly relayer](https://github.com/SublyFi/subly-payment-protocol/tree/main/deploy); no Subly account is required.
|
|
4
4
|
|
|
5
|
-
Version 0.
|
|
5
|
+
Version 0.8 is beta software and has not had an external security audit. Vault operations use real mainnet funds. Yield accounting and owner policies depend on your relayer operator. Read the [security model](https://github.com/SublyFi/subly-payment-protocol/blob/main/docs/security-model.md).
|
|
6
6
|
|
|
7
7
|
## Quick start
|
|
8
8
|
|
|
9
9
|
You need **Node.js 24+**, a Solana agent wallet with mainnet USDC, a trusted relayer URL, and a mainnet RPC endpoint that supports transaction simulation with inner instructions. Your wallet can be a local keypair or a supported custody signer. The owner who approves spending controls can use a passkey or a separate Solana wallet.
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y @subly_fi/pay@0.
|
|
12
|
+
npx -y @subly_fi/pay@0.8.0 --help
|
|
13
13
|
export SUBLY_RELAYER_URL=https://your-relayer.example.com
|
|
14
14
|
export SOLANA_RPC_URL=https://your-mainnet-rpc.example.com
|
|
15
15
|
export SUBLY_DEMO_AGENT_KEYPAIR_PATH=/absolute/path/to/agent.json
|
|
16
|
-
npx -y @subly_fi/pay@0.
|
|
17
|
-
npx -y @subly_fi/pay@0.
|
|
16
|
+
npx -y @subly_fi/pay@0.8.0 doctor
|
|
17
|
+
npx -y @subly_fi/pay@0.8.0 vaults
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
Use an existing dedicated agent wallet, or create one with `solana-keygen new -o agent.json`. Keep its recovery material private and restrict the file to its owner (`chmod 600 agent.json`). Fund its public address with **USDC on Solana mainnet**. Subly does not create or fund wallets. Vault fees require a funded relayer sponsor; the final API payment requires the seller's facilitator fee payer.
|
|
@@ -23,34 +23,38 @@ Use an existing dedicated agent wallet, or create one with `solana-keygen new -o
|
|
|
23
23
|
2. Create an owner setup link. On the first owner registration, this example pre-approves a **1.01 USDC** deposit; the selected vault's minimum can differ:
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
npx -y @subly_fi/pay@0.
|
|
26
|
+
npx -y @subly_fi/pay@0.8.0 setup-link --initial-deposit 1010000
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
Open the returned `setupUrl`, review the wallet, vault and limits, then approve with your passkey or wallet. Links expire in 10 minutes. Treat setup and approval links as private capabilities. The first person completing an initial setup becomes the owner for that wallet/vault. Replacing an existing mandate requires a separate deposit approval; follow the approval link returned by the deposit command.
|
|
30
30
|
3. Check completion and deposit promptly; initial deposit approval lasts about 15 minutes:
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
|
-
npx -y @subly_fi/pay@0.
|
|
34
|
-
npx -y @subly_fi/pay@0.
|
|
35
|
-
npx -y @subly_fi/pay@0.
|
|
33
|
+
npx -y @subly_fi/pay@0.8.0 setup-status <sessionId>
|
|
34
|
+
npx -y @subly_fi/pay@0.8.0 deposit 1010000
|
|
35
|
+
npx -y @subly_fi/pay@0.8.0 budget
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
4. Wait until **spendable yield** covers the price and vault fees. A new deposit does not immediately provide a payment budget. Then request a compatible API:
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
npx -y @subly_fi/pay@0.
|
|
41
|
+
npx -y @subly_fi/pay@0.8.0 fetch https://seller.example.com/paid-resource
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
5. Withdraw funds back to the same agent wallet when needed:
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
|
-
npx -y @subly_fi/pay@0.
|
|
47
|
+
npx -y @subly_fi/pay@0.8.0 withdraw 1000000
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
All amounts are raw USDC integers: `1000000` = 1 USDC. `fetch` defaults to a **0.01 USDC cap**, configurable with `SUBLY_MCP_MAX_AMOUNT_RAW_USDC` or `fetch <URL> <capRawUSDC>`. The owner mandate may set stricter limits. `setup-link --help` lists policy options. A withdrawal can include principal and is subject to liquidity, fees and the owner's policy. A revoked mandate also blocks relayer withdrawals.
|
|
51
51
|
|
|
52
52
|
A subsequent deposit/payment/withdrawal may return `approvalRequired` with an `approveUrl`. After the owner approves, retry the same operation with the returned `apr_...` as a trailing argument. Never automatically retry a transaction reported as submitted or an API payment with an unknown outcome.
|
|
53
53
|
|
|
54
|
+
Status lookup requires a **0.8.0 or newer relayer** so it can disable rebroadcast of an unresolved transaction with the authenticated `?resubmit=false` option. Upgrade the relayer before using this command.
|
|
55
|
+
|
|
56
|
+
For an interrupted deposit or withdrawal, keep its `depositId` (`dep_...`) or `withdrawalId` (`wdr_...`) and run `pay status <intentId>` with the same wallet, selected vault and relayer. This reads and reconciles the original operation; it does not prepare, sign or send another transaction. The result includes the requested/confirmed amount, transaction signature and next action. `submitted` means check the same ID again later. A successful status command exits zero even when the original operation is still pending or failed; inspect its `status` and `nextAction`. Wallet-auth message signing is required, but no client RPC call is needed.
|
|
57
|
+
|
|
54
58
|
## MCP configuration
|
|
55
59
|
|
|
56
60
|
Add this to the MCP configuration of your editor or agent host. Replace all example values with your own absolute paths and endpoints. Pinning the version keeps upgrades explicit.
|
|
@@ -60,7 +64,7 @@ Add this to the MCP configuration of your editor or agent host. Replace all exam
|
|
|
60
64
|
"mcpServers": {
|
|
61
65
|
"subly": {
|
|
62
66
|
"command": "npx",
|
|
63
|
-
"args": ["-y", "@subly_fi/pay@0.
|
|
67
|
+
"args": ["-y", "@subly_fi/pay@0.8.0", "mcp"],
|
|
64
68
|
"env": {
|
|
65
69
|
"SUBLY_RELAYER_URL": "https://your-relayer.example.com",
|
|
66
70
|
"SOLANA_RPC_URL": "https://your-mainnet-rpc.example.com",
|
|
@@ -73,7 +77,7 @@ Add this to the MCP configuration of your editor or agent host. Replace all exam
|
|
|
73
77
|
}
|
|
74
78
|
```
|
|
75
79
|
|
|
76
|
-
Tools: `list_subly_vaults`, `select_subly_vault`, `create_subly_setup_link`, `check_subly_setup`, `deposit_to_subly_vault`, `get_subly_yield_budget`, `withdraw_from_subly_vault`, `fetch_with_subly_payment`. Ask the agent to list vaults and follow owner setup before depositing. Each selected vault has its own mandate and accounting; changing selection never moves funds. Stdio stdout is reserved for MCP messages.
|
|
80
|
+
Tools: `list_subly_vaults`, `select_subly_vault`, `create_subly_setup_link`, `check_subly_setup`, `check_subly_vault_operation`, `deposit_to_subly_vault`, `get_subly_yield_budget`, `withdraw_from_subly_vault`, `fetch_with_subly_payment`. Ask the agent to list vaults and follow owner setup before depositing. Each selected vault has its own mandate and accounting; changing selection never moves funds. Stdio stdout is reserved for MCP messages.
|
|
77
81
|
|
|
78
82
|
## Configuration
|
|
79
83
|
|
|
@@ -99,8 +103,10 @@ Only sellers offering **Solana mainnet USDC `exact`** with `extra.feePayer` are
|
|
|
99
103
|
- `doctor` performs read-only configuration, relayer/vault and RPC checks. It never signs or transacts; it does not prove vault safety or available yield.
|
|
100
104
|
- A withdrawal preview failure is a refusal to sign. Check your RPC's simulation support and liquidity; do not disable transaction validation.
|
|
101
105
|
- Preserve the pending-state JSON across restarts and upgrades. An `external_outcome_unknown` record blocks a second payment until you investigate the seller/facilitator outcome.
|
|
106
|
+
- When `fetch` stops while realizing yield, retry the same request with the same wallet, vault, relayer, method, body and headers. A saved withdrawal checkpoint resumes the original withdrawal and reuses its confirmed funds. It never prepares a replacement during recovery, even with `forceNewPayment`. If interruption occurred before the withdrawal ID was saved, or the original withdrawal ended unsuccessfully, reconcile that operation with the operator before starting another payment.
|
|
107
|
+
- Older pending records remain blocked for investigation because they do not identify a resumable withdrawal. Keep the state file; do not downgrade the client while a realization is pending.
|
|
102
108
|
- Concurrent clients using the same state file serialize payments with a `.lock` file. After a crash, stop **all** clients using that file before removing only the stale `.lock`. Preserve the JSON. A new file or another machine cannot coordinate with the old one.
|
|
103
|
-
- `submitted` means the transaction may still confirm.
|
|
109
|
+
- `submitted` means the transaction may still confirm. For a manual deposit or withdrawal, run `pay status <intentId>` or call `check_subly_vault_operation` with `intentId` instead of repeating the operation. Keep the original wallet, selected vault and relayer. An API `fetch` uses the saved recovery checkpoint described above.
|
|
104
110
|
- Setup passkeys bind to the operator's domain. Use the original domain and device credential; follow the documented recovery delay if access is lost.
|
|
105
111
|
|
|
106
112
|
[Full troubleshooting](https://github.com/SublyFi/subly-payment-protocol/blob/main/docs/troubleshooting.md) · [Support](https://github.com/SublyFi/subly-payment-protocol/blob/main/SUPPORT.md) · [Private security reports](https://github.com/SublyFi/subly-payment-protocol/security/advisories/new)
|
package/dist/budget.js
CHANGED
|
@@ -1790,6 +1790,18 @@ async function assertWithdrawalPreview(input) {
|
|
|
1790
1790
|
}
|
|
1791
1791
|
}
|
|
1792
1792
|
|
|
1793
|
+
// ../../src/lib/canonical-json.ts
|
|
1794
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
1795
|
+
function canonicalJson2(value) {
|
|
1796
|
+
return stableStringify(value);
|
|
1797
|
+
}
|
|
1798
|
+
function sha256HexOf(data) {
|
|
1799
|
+
return createHash3("sha256").update(data, "utf8").digest("hex");
|
|
1800
|
+
}
|
|
1801
|
+
function canonicalJsonHash(value) {
|
|
1802
|
+
return sha256HexOf(canonicalJson2(value));
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1793
1805
|
// ../../src/client/lookup-tables.ts
|
|
1794
1806
|
import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
|
|
1795
1807
|
import { address, getCompiledTransactionMessageDecoder as getCompiledTransactionMessageDecoder2 } from "@solana/kit";
|
|
@@ -1831,14 +1843,14 @@ async function fetchLookupTablesForTransaction(rpc, serializedTransaction) {
|
|
|
1831
1843
|
}
|
|
1832
1844
|
|
|
1833
1845
|
// ../../src/api/wallet-auth.ts
|
|
1834
|
-
import { createHash as
|
|
1846
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
1835
1847
|
import bs587 from "bs58";
|
|
1836
1848
|
import nacl3 from "tweetnacl";
|
|
1837
1849
|
var WALLET_AUTH_WALLET_HEADER = "x-subly-wallet";
|
|
1838
1850
|
var WALLET_AUTH_SIGNED_AT_HEADER = "x-subly-signed-at";
|
|
1839
1851
|
var WALLET_AUTH_SIGNATURE_HEADER = "x-subly-signature";
|
|
1840
1852
|
function sha256Hex(data) {
|
|
1841
|
-
return
|
|
1853
|
+
return createHash4("sha256").update(data, "utf8").digest("hex");
|
|
1842
1854
|
}
|
|
1843
1855
|
function walletAuthMessage(params) {
|
|
1844
1856
|
return new TextEncoder().encode(
|
|
@@ -1882,6 +1894,11 @@ var VaultFlowClientError = class extends Error {
|
|
|
1882
1894
|
code;
|
|
1883
1895
|
errorDetails;
|
|
1884
1896
|
};
|
|
1897
|
+
function vaultOperationKind(intentId) {
|
|
1898
|
+
if (/^dep_[0-9a-f]{32}$/.test(intentId)) return "deposit";
|
|
1899
|
+
if (/^wdr_[0-9a-f]{32}$/.test(intentId)) return "withdrawal";
|
|
1900
|
+
throw new VaultFlowClientError("read", "intentId must be the original dep_ or wdr_ ID followed by 32 lowercase hexadecimal characters");
|
|
1901
|
+
}
|
|
1885
1902
|
var VaultFlowClient = class {
|
|
1886
1903
|
vault;
|
|
1887
1904
|
rpc;
|
|
@@ -1984,9 +2001,36 @@ var VaultFlowClient = class {
|
|
|
1984
2001
|
...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
|
|
1985
2002
|
}
|
|
1986
2003
|
);
|
|
2004
|
+
this.assertPreparedWithdrawal(prepared, input);
|
|
2005
|
+
await input.onPrepared?.(prepared);
|
|
2006
|
+
return this.submitPreparedWithdrawal(prepared, input);
|
|
2007
|
+
}
|
|
2008
|
+
/** Reconcile or submit the original intent; never prepare a replacement. */
|
|
2009
|
+
async resumeWithdrawal(prepared, input) {
|
|
2010
|
+
this.assertPreparedWithdrawal(prepared, input);
|
|
2011
|
+
const current = await this.getJson(
|
|
2012
|
+
`/v1/withdrawals/${encodeURIComponent(prepared.withdrawalId)}`
|
|
2013
|
+
);
|
|
2014
|
+
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) {
|
|
2015
|
+
throw new VaultFlowClientError("read", "Saved withdrawal differs from the original operation; refusing to resume");
|
|
2016
|
+
}
|
|
2017
|
+
if (current.status === "prepared") {
|
|
2018
|
+
return this.submitPreparedWithdrawal(prepared, input);
|
|
2019
|
+
}
|
|
2020
|
+
if (!["submitted", "confirmed", "failed", "failed_not_submitted", "expired", "quarantined"].includes(current.status)) {
|
|
2021
|
+
throw new VaultFlowClientError("read", "Relayer returned an unknown withdrawal status");
|
|
2022
|
+
}
|
|
2023
|
+
return this.withdrawalOutcome(prepared, current);
|
|
2024
|
+
}
|
|
2025
|
+
assertPreparedWithdrawal(prepared, input) {
|
|
1987
2026
|
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) {
|
|
1988
2027
|
throw new VaultFlowClientError("prepare", "Prepared withdrawal differs from the requested operation");
|
|
1989
2028
|
}
|
|
2029
|
+
if (typeof prepared.withdrawalId !== "string" || prepared.withdrawalId.length === 0) {
|
|
2030
|
+
throw new VaultFlowClientError("prepare", "Prepared withdrawal has no withdrawal ID");
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
async submitPreparedWithdrawal(prepared, input) {
|
|
1990
2034
|
await assertWithdrawalPreview({
|
|
1991
2035
|
rpc: this.rpc,
|
|
1992
2036
|
serializedTransaction: prepared.serializedTransaction,
|
|
@@ -2000,6 +2044,7 @@ var VaultFlowClient = class {
|
|
|
2000
2044
|
serializedTransaction: prepared.serializedTransaction,
|
|
2001
2045
|
lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
|
|
2002
2046
|
});
|
|
2047
|
+
input.onBeforeSubmit?.();
|
|
2003
2048
|
let outcome = await this.postJson("submit", "/v1/withdrawals/submit", {
|
|
2004
2049
|
withdrawalId: prepared.withdrawalId,
|
|
2005
2050
|
serializedTransaction: signed.serializedTransaction,
|
|
@@ -2011,6 +2056,9 @@ var VaultFlowClient = class {
|
|
|
2011
2056
|
outcome
|
|
2012
2057
|
);
|
|
2013
2058
|
}
|
|
2059
|
+
return this.withdrawalOutcome(prepared, outcome);
|
|
2060
|
+
}
|
|
2061
|
+
withdrawalOutcome(prepared, outcome) {
|
|
2014
2062
|
return {
|
|
2015
2063
|
withdrawalId: prepared.withdrawalId,
|
|
2016
2064
|
status: outcome.status,
|
|
@@ -2021,6 +2069,40 @@ var VaultFlowClient = class {
|
|
|
2021
2069
|
errorCode: outcome.errorCode ?? null
|
|
2022
2070
|
};
|
|
2023
2071
|
}
|
|
2072
|
+
/** Authenticated read/reconciliation only: never prepare, sign or submit a transaction. */
|
|
2073
|
+
async getOperationStatus(intentId) {
|
|
2074
|
+
const kind = vaultOperationKind(intentId);
|
|
2075
|
+
const raw = await this.getJson(`/v1/${kind === "deposit" ? "deposits" : "withdrawals"}/${intentId}?resubmit=false`);
|
|
2076
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
2077
|
+
throw new VaultFlowClientError("read", "Relayer returned an invalid operation status");
|
|
2078
|
+
}
|
|
2079
|
+
const record = raw;
|
|
2080
|
+
if (record[kind === "deposit" ? "depositId" : "withdrawalId"] !== intentId || record.wallet !== this.signer.walletAddress || record.vault !== this.vault.address) {
|
|
2081
|
+
throw new VaultFlowClientError("read", "Operation does not match the requested ID, current wallet or selected vault; use the original wallet, vault and relayer");
|
|
2082
|
+
}
|
|
2083
|
+
const requested = record[kind === "deposit" ? "amountRawUsdc" : "requestedWithdrawRawUsdc"];
|
|
2084
|
+
const actual = record[kind === "deposit" ? "actualDepositRawUsdc" : "actualWithdrawRawUsdc"];
|
|
2085
|
+
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)) {
|
|
2086
|
+
throw new VaultFlowClientError("read", "Relayer returned incomplete or invalid operation status fields");
|
|
2087
|
+
}
|
|
2088
|
+
const status = record.status;
|
|
2089
|
+
const nextAction = status === "confirmed" ? "done" : status === "submitted" || status === "prepared" ? "check_again" : "reconcile_with_operator";
|
|
2090
|
+
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.";
|
|
2091
|
+
return {
|
|
2092
|
+
intentId,
|
|
2093
|
+
kind,
|
|
2094
|
+
wallet: this.signer.walletAddress,
|
|
2095
|
+
vault: this.vault.address,
|
|
2096
|
+
status,
|
|
2097
|
+
requestedAmountRawUsdc: requested,
|
|
2098
|
+
actualAmountRawUsdc: actual,
|
|
2099
|
+
txSignature: record.txSignature,
|
|
2100
|
+
errorCode: record.errorCode,
|
|
2101
|
+
stillConfirming: status === "submitted",
|
|
2102
|
+
nextAction,
|
|
2103
|
+
message
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2024
2106
|
/**
|
|
2025
2107
|
* Reads the yield budget. Syncs the relayer's ledger from chain first (so
|
|
2026
2108
|
* yield accrued since the last sync shows up); the sync is best-effort and
|
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ const here = dirname(fileURLToPath(import.meta.url));
|
|
|
6
6
|
const TARGETS = {
|
|
7
7
|
mcp: "mcp-server.js", fetch: "pay.js", deposit: "deposit.js", withdraw: "withdraw.js",
|
|
8
8
|
"setup-link": "setup-link.js", "setup-status": "setup-status.js",
|
|
9
|
-
doctor: "doctor.js", budget: "budget.js", vaults: "vaults.js"
|
|
9
|
+
doctor: "doctor.js", budget: "budget.js", vaults: "vaults.js", status: "status.js"
|
|
10
10
|
};
|
|
11
11
|
const HELP = `Subly — x402 payments from Kamino USDC vault yield
|
|
12
12
|
|
|
@@ -18,6 +18,7 @@ Usage: pay <command> [arguments]
|
|
|
18
18
|
deposit <rawUSDC> [approvalId] Deposit into the selected vault (real funds)
|
|
19
19
|
budget Refresh and read the selected vault's yield budget
|
|
20
20
|
withdraw <rawUSDC> [approvalId] Withdraw to the agent wallet (real funds)
|
|
21
|
+
status <dep_...|wdr_...> Check the original deposit/withdrawal (no new transaction)
|
|
21
22
|
fetch <URL> Pay a compatible x402 API within the configured cap
|
|
22
23
|
mcp Start the stdio MCP server
|
|
23
24
|
--version Print package version
|
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
|
|
@@ -2418,12 +2500,12 @@ if (submitted.txSignature !== null) {
|
|
|
2418
2500
|
}
|
|
2419
2501
|
if (submitted.status === "submitted") {
|
|
2420
2502
|
fail(
|
|
2421
|
-
`[deposit] broadcast but not yet confirmed \u2014 it may still land. Do NOT resubmit;
|
|
2503
|
+
`[deposit] broadcast but not yet confirmed \u2014 it may still land. Do NOT resubmit; run pay status ${submitted.depositId} with the same wallet, vault and relayer first`
|
|
2422
2504
|
);
|
|
2423
2505
|
}
|
|
2424
2506
|
if (submitted.status !== "confirmed") {
|
|
2425
2507
|
fail(
|
|
2426
|
-
`[deposit] not confirmed (errorCode=${submitted.errorCode});
|
|
2508
|
+
`[deposit] not confirmed (errorCode=${submitted.errorCode}); run pay status ${submitted.depositId} and ask the operator to reconcile it`
|
|
2427
2509
|
);
|
|
2428
2510
|
}
|
|
2429
2511
|
console.log(
|