sellbonds-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +63 -0
- package/dist/index.js +1181 -0
- package/package.json +49 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1181 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
// ../sdk/dist/index.js
|
|
9
|
+
import { getAddress, isAddress } from "viem";
|
|
10
|
+
import { base, baseSepolia } from "viem/chains";
|
|
11
|
+
import {
|
|
12
|
+
createPublicClient,
|
|
13
|
+
createWalletClient,
|
|
14
|
+
fallback,
|
|
15
|
+
http
|
|
16
|
+
} from "viem";
|
|
17
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
18
|
+
import { homedir } from "os";
|
|
19
|
+
import { dirname, join } from "path";
|
|
20
|
+
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
|
|
21
|
+
import { parseEther } from "viem";
|
|
22
|
+
import { randomBytes } from "crypto";
|
|
23
|
+
import {
|
|
24
|
+
encodeAbiParameters,
|
|
25
|
+
getAddress as getAddress2,
|
|
26
|
+
parseAbiParameters,
|
|
27
|
+
zeroAddress
|
|
28
|
+
} from "viem";
|
|
29
|
+
import { BaseError, ContractFunctionRevertedError } from "viem";
|
|
30
|
+
import { formatUnits, parseUnits } from "viem";
|
|
31
|
+
var base_default = {
|
|
32
|
+
chainId: 8453,
|
|
33
|
+
chain: "base",
|
|
34
|
+
label: "Base",
|
|
35
|
+
rpcUrl: "https://mainnet.base.org",
|
|
36
|
+
explorer: "https://basescan.org",
|
|
37
|
+
faucetUrl: "",
|
|
38
|
+
testnet: false,
|
|
39
|
+
deployed: true,
|
|
40
|
+
deployer: "0x223bb3aa4418a99180814b4d4ab614bafaeda2de",
|
|
41
|
+
contracts: {
|
|
42
|
+
WildcatArchController: "0x0dbb4426844266add3ab840935cd1a3a67dd4ef6",
|
|
43
|
+
WildcatSanctionsSentinel: "0x402ead2d9aaeedfb53fda4b39fc668c6545beee9",
|
|
44
|
+
NullSanctionsOracle: "0x23c8d3ab08e998919d9a1f98a4eb6c5da6c01ae7",
|
|
45
|
+
HooksFactory: "0x6fe029dfc85924c83a2f7159292f53b3a9a3806f",
|
|
46
|
+
OpenTermHooksTemplate: "0xf45cd0d37b5fdf4b5e54d40d1c95c43018c4b55a",
|
|
47
|
+
FixedTermHooksTemplate: "0x2c37ce66c2e9c37789d659e02657faa83ac3d7aa",
|
|
48
|
+
TestUSDC: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
|
|
49
|
+
WildcatMarketInitCodeStorage: "0x29741d542e1f6a6ad0ee3e7734cf478755630a24",
|
|
50
|
+
BondNotes: "0xbe5369cfcbe284d42306bc462be796a7c764dbe9"
|
|
51
|
+
},
|
|
52
|
+
deployedAt: 1780758919
|
|
53
|
+
};
|
|
54
|
+
var base_sepolia_default = {
|
|
55
|
+
chainId: 84532,
|
|
56
|
+
chain: "base-sepolia",
|
|
57
|
+
label: "Base Sepolia (testnet)",
|
|
58
|
+
rpcUrl: "https://sepolia.base.org",
|
|
59
|
+
explorer: "https://sepolia.basescan.org",
|
|
60
|
+
faucetUrl: "https://sellbonds.now/api/faucet",
|
|
61
|
+
testnet: true,
|
|
62
|
+
deployed: true,
|
|
63
|
+
deployer: "0x223bb3aa4418a99180814b4d4ab614bafaeda2de",
|
|
64
|
+
contracts: {
|
|
65
|
+
WildcatArchController: "0x0dbb4426844266add3ab840935cd1a3a67dd4ef6",
|
|
66
|
+
WildcatSanctionsSentinel: "0x402ead2d9aaeedfb53fda4b39fc668c6545beee9",
|
|
67
|
+
NullSanctionsOracle: "0x23c8d3ab08e998919d9a1f98a4eb6c5da6c01ae7",
|
|
68
|
+
HooksFactory: "0x6fe029dfc85924c83a2f7159292f53b3a9a3806f",
|
|
69
|
+
OpenTermHooksTemplate: "0xf45cd0d37b5fdf4b5e54d40d1c95c43018c4b55a",
|
|
70
|
+
FixedTermHooksTemplate: "0x2c37ce66c2e9c37789d659e02657faa83ac3d7aa",
|
|
71
|
+
TestUSDC: "0xbe5369cfcbe284d42306bc462be796a7c764dbe9",
|
|
72
|
+
WildcatMarketInitCodeStorage: "0x29741d542e1f6a6ad0ee3e7734cf478755630a24"
|
|
73
|
+
},
|
|
74
|
+
deployedAt: 1780320142
|
|
75
|
+
};
|
|
76
|
+
var ZERO = "0x0000000000000000000000000000000000000000";
|
|
77
|
+
var DEPLOYMENTS = {
|
|
78
|
+
base: base_default,
|
|
79
|
+
"base-sepolia": base_sepolia_default
|
|
80
|
+
};
|
|
81
|
+
var DEFAULT_NETWORK = "base";
|
|
82
|
+
function networkName() {
|
|
83
|
+
return process.env.SBN_NETWORK?.trim() || DEFAULT_NETWORK;
|
|
84
|
+
}
|
|
85
|
+
function loadDeployment(network = networkName()) {
|
|
86
|
+
const base2 = DEPLOYMENTS[network];
|
|
87
|
+
if (!base2) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Unknown network "${network}". Known networks: ${Object.keys(DEPLOYMENTS).join(", ")}.`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
const d = JSON.parse(JSON.stringify(base2));
|
|
93
|
+
const env = process.env;
|
|
94
|
+
if (env.SBN_RPC_URL?.trim()) d.rpcUrl = env.SBN_RPC_URL.trim();
|
|
95
|
+
if (env.SBN_EXPLORER?.trim()) d.explorer = env.SBN_EXPLORER.trim();
|
|
96
|
+
if (env.SBN_FAUCET_URL?.trim()) d.faucetUrl = env.SBN_FAUCET_URL.trim();
|
|
97
|
+
const overrideAddr = (envKey, target) => {
|
|
98
|
+
const v = env[envKey]?.trim();
|
|
99
|
+
if (v && isAddress(v)) d.contracts[target] = getAddress(v);
|
|
100
|
+
};
|
|
101
|
+
overrideAddr("SBN_ARCH_CONTROLLER", "WildcatArchController");
|
|
102
|
+
overrideAddr("SBN_HOOKS_FACTORY", "HooksFactory");
|
|
103
|
+
overrideAddr("SBN_OPEN_TERM_TEMPLATE", "OpenTermHooksTemplate");
|
|
104
|
+
overrideAddr("SBN_FIXED_TERM_TEMPLATE", "FixedTermHooksTemplate");
|
|
105
|
+
overrideAddr("SBN_USDC", "TestUSDC");
|
|
106
|
+
d.deployed = d.contracts.WildcatArchController !== ZERO && d.contracts.HooksFactory !== ZERO && d.contracts.OpenTermHooksTemplate !== ZERO;
|
|
107
|
+
return d;
|
|
108
|
+
}
|
|
109
|
+
function assertDeployed(d) {
|
|
110
|
+
if (!d.deployed) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`sellbonds.now is not deployed on ${d.label} yet. If you just deployed, set SBN_ARCH_CONTROLLER / SBN_HOOKS_FACTORY / SBN_OPEN_TERM_TEMPLATE / SBN_FIXED_TERM_TEMPLATE / SBN_USDC, or rebuild sellbonds with the populated deployments file.`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function viemChain(d) {
|
|
117
|
+
if (d.chainId === base.id) return base;
|
|
118
|
+
if (d.chainId === baseSepolia.id) return baseSepolia;
|
|
119
|
+
return {
|
|
120
|
+
id: d.chainId,
|
|
121
|
+
name: d.label,
|
|
122
|
+
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
|
|
123
|
+
rpcUrls: { default: { http: [d.rpcUrl] } },
|
|
124
|
+
blockExplorers: { default: { name: "Explorer", url: d.explorer } }
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function txUrl(d, hash) {
|
|
128
|
+
return `${d.explorer}/tx/${hash}`;
|
|
129
|
+
}
|
|
130
|
+
function addressUrl(d, address) {
|
|
131
|
+
return `${d.explorer}/address/${address}`;
|
|
132
|
+
}
|
|
133
|
+
var SBN_HOME = process.env.SBN_HOME?.trim() || join(homedir(), ".sellbondsnow");
|
|
134
|
+
var WALLET_PATH = join(SBN_HOME, "wallet.json");
|
|
135
|
+
var MARKETS_PATH = join(SBN_HOME, "markets.json");
|
|
136
|
+
var KEYSTORE_NOTE = "sellbonds.now agent wallet. PLAINTEXT testnet key, mode 0600. Do not reuse this key on mainnet with real funds. Back it up if you care about the markets it controls.";
|
|
137
|
+
function ensureHome() {
|
|
138
|
+
if (!existsSync(SBN_HOME)) mkdirSync(SBN_HOME, { recursive: true, mode: 448 });
|
|
139
|
+
}
|
|
140
|
+
function walletExists() {
|
|
141
|
+
return existsSync(WALLET_PATH);
|
|
142
|
+
}
|
|
143
|
+
function createWallet() {
|
|
144
|
+
ensureHome();
|
|
145
|
+
const privateKey = generatePrivateKey();
|
|
146
|
+
const account2 = privateKeyToAccount(privateKey);
|
|
147
|
+
const wallet = {
|
|
148
|
+
address: account2.address,
|
|
149
|
+
privateKey,
|
|
150
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
151
|
+
note: KEYSTORE_NOTE
|
|
152
|
+
};
|
|
153
|
+
writeWallet(wallet);
|
|
154
|
+
return wallet;
|
|
155
|
+
}
|
|
156
|
+
function writeWallet(wallet) {
|
|
157
|
+
ensureHome();
|
|
158
|
+
writeFileSync(WALLET_PATH, JSON.stringify(wallet, null, 2), { mode: 384 });
|
|
159
|
+
chmodSync(WALLET_PATH, 384);
|
|
160
|
+
}
|
|
161
|
+
function loadWallet() {
|
|
162
|
+
if (!walletExists()) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`No wallet found at ${WALLET_PATH}. Create one with \`sbn wallet new\` (or it will be created automatically by \`sbn raise\`).`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return JSON.parse(readFileSync(WALLET_PATH, "utf8"));
|
|
168
|
+
}
|
|
169
|
+
function loadOrCreateWallet() {
|
|
170
|
+
if (walletExists()) return { wallet: loadWallet(), created: false };
|
|
171
|
+
return { wallet: createWallet(), created: true };
|
|
172
|
+
}
|
|
173
|
+
function account() {
|
|
174
|
+
const envKey = process.env.SBN_PRIVATE_KEY?.trim();
|
|
175
|
+
if (envKey) {
|
|
176
|
+
const hex = envKey.startsWith("0x") ? envKey : `0x${envKey}`;
|
|
177
|
+
return privateKeyToAccount(hex);
|
|
178
|
+
}
|
|
179
|
+
return privateKeyToAccount(loadWallet().privateKey);
|
|
180
|
+
}
|
|
181
|
+
function recordMarket(record) {
|
|
182
|
+
ensureHome();
|
|
183
|
+
const existing = listMarkets();
|
|
184
|
+
existing.push(record);
|
|
185
|
+
writeFileSync(MARKETS_PATH, JSON.stringify(existing, null, 2), { mode: 384 });
|
|
186
|
+
}
|
|
187
|
+
function listMarkets() {
|
|
188
|
+
if (!existsSync(MARKETS_PATH)) return [];
|
|
189
|
+
try {
|
|
190
|
+
return JSON.parse(readFileSync(MARKETS_PATH, "utf8"));
|
|
191
|
+
} catch {
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function rpcTransport(d) {
|
|
196
|
+
const candidates = [
|
|
197
|
+
d.rpcUrl,
|
|
198
|
+
d.testnet ? void 0 : "https://sellbonds.now/rpc",
|
|
199
|
+
d.testnet ? "https://sepolia.base.org" : "https://mainnet.base.org"
|
|
200
|
+
].filter((u) => !!u);
|
|
201
|
+
const seen = /* @__PURE__ */ new Set();
|
|
202
|
+
const urls = candidates.filter((u) => {
|
|
203
|
+
const k = u.toLowerCase();
|
|
204
|
+
if (seen.has(k)) return false;
|
|
205
|
+
seen.add(k);
|
|
206
|
+
return true;
|
|
207
|
+
});
|
|
208
|
+
return fallback(urls.map((u) => http(u, { retryCount: 1 })));
|
|
209
|
+
}
|
|
210
|
+
function connect(network, acct = account()) {
|
|
211
|
+
const deployment = loadDeployment(network);
|
|
212
|
+
const chain = viemChain(deployment);
|
|
213
|
+
const transport2 = rpcTransport(deployment);
|
|
214
|
+
const publicClient = createPublicClient({ chain, transport: transport2 });
|
|
215
|
+
const walletClient = createWalletClient({ account: acct, chain, transport: transport2 });
|
|
216
|
+
return { deployment, publicClient, walletClient, account: acct };
|
|
217
|
+
}
|
|
218
|
+
function connectReadonly(network) {
|
|
219
|
+
const deployment = loadDeployment(network);
|
|
220
|
+
const chain = viemChain(deployment);
|
|
221
|
+
const publicClient = createPublicClient({ chain, transport: rpcTransport(deployment) });
|
|
222
|
+
return { deployment, publicClient };
|
|
223
|
+
}
|
|
224
|
+
async function requestEthFromFaucet(faucetUrl, address) {
|
|
225
|
+
try {
|
|
226
|
+
const res = await fetch(faucetUrl, {
|
|
227
|
+
method: "POST",
|
|
228
|
+
headers: { "Content-Type": "application/json" },
|
|
229
|
+
body: JSON.stringify({ address })
|
|
230
|
+
});
|
|
231
|
+
let body = {};
|
|
232
|
+
try {
|
|
233
|
+
body = await res.json();
|
|
234
|
+
} catch {
|
|
235
|
+
}
|
|
236
|
+
if (!res.ok) {
|
|
237
|
+
return { ok: false, message: body?.error || body?.message || `faucet returned ${res.status}` };
|
|
238
|
+
}
|
|
239
|
+
return { ok: true, txHash: body?.txHash, message: body?.message };
|
|
240
|
+
} catch (err) {
|
|
241
|
+
return { ok: false, message: err instanceof Error ? err.message : String(err) };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
var archControllerAbi = [
|
|
245
|
+
{ type: "function", name: "registerSelf", inputs: [], outputs: [], stateMutability: "nonpayable" },
|
|
246
|
+
{
|
|
247
|
+
type: "function",
|
|
248
|
+
name: "isRegisteredBorrower",
|
|
249
|
+
inputs: [{ name: "borrower", type: "address" }],
|
|
250
|
+
outputs: [{ type: "bool" }],
|
|
251
|
+
stateMutability: "view"
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
type: "function",
|
|
255
|
+
name: "getRegisteredMarkets",
|
|
256
|
+
inputs: [],
|
|
257
|
+
outputs: [{ type: "address[]" }],
|
|
258
|
+
stateMutability: "view"
|
|
259
|
+
},
|
|
260
|
+
{ type: "event", name: "BorrowerAdded", inputs: [{ name: "borrower", type: "address", indexed: true }] },
|
|
261
|
+
{ type: "error", name: "BorrowerAlreadyExists", inputs: [] }
|
|
262
|
+
];
|
|
263
|
+
var deployMarketInputs = {
|
|
264
|
+
name: "parameters",
|
|
265
|
+
type: "tuple",
|
|
266
|
+
components: [
|
|
267
|
+
{ name: "asset", type: "address" },
|
|
268
|
+
{ name: "namePrefix", type: "string" },
|
|
269
|
+
{ name: "symbolPrefix", type: "string" },
|
|
270
|
+
{ name: "maxTotalSupply", type: "uint128" },
|
|
271
|
+
{ name: "annualInterestBips", type: "uint16" },
|
|
272
|
+
{ name: "delinquencyFeeBips", type: "uint16" },
|
|
273
|
+
{ name: "withdrawalBatchDuration", type: "uint32" },
|
|
274
|
+
{ name: "reserveRatioBips", type: "uint16" },
|
|
275
|
+
{ name: "delinquencyGracePeriod", type: "uint32" },
|
|
276
|
+
{ name: "hooks", type: "uint256" }
|
|
277
|
+
]
|
|
278
|
+
};
|
|
279
|
+
var hooksFactoryAbi = [
|
|
280
|
+
{
|
|
281
|
+
type: "function",
|
|
282
|
+
name: "deployMarketAndHooks",
|
|
283
|
+
inputs: [
|
|
284
|
+
{ name: "hooksTemplate", type: "address" },
|
|
285
|
+
{ name: "hooksTemplateArgs", type: "bytes" },
|
|
286
|
+
deployMarketInputs,
|
|
287
|
+
{ name: "hooksData", type: "bytes" },
|
|
288
|
+
{ name: "salt", type: "bytes32" },
|
|
289
|
+
{ name: "originationFeeAsset", type: "address" },
|
|
290
|
+
{ name: "originationFeeAmount", type: "uint256" }
|
|
291
|
+
],
|
|
292
|
+
outputs: [
|
|
293
|
+
{ name: "market", type: "address" },
|
|
294
|
+
{ name: "hooksInstance", type: "address" }
|
|
295
|
+
],
|
|
296
|
+
stateMutability: "nonpayable"
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
type: "function",
|
|
300
|
+
name: "computeMarketAddress",
|
|
301
|
+
inputs: [{ name: "salt", type: "bytes32" }],
|
|
302
|
+
outputs: [{ type: "address" }],
|
|
303
|
+
stateMutability: "view"
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
type: "event",
|
|
307
|
+
name: "MarketDeployed",
|
|
308
|
+
inputs: [
|
|
309
|
+
{ name: "hooksTemplate", type: "address", indexed: true },
|
|
310
|
+
{ name: "market", type: "address", indexed: true },
|
|
311
|
+
{ name: "name", type: "string", indexed: false },
|
|
312
|
+
{ name: "symbol", type: "string", indexed: false },
|
|
313
|
+
{ name: "asset", type: "address", indexed: false },
|
|
314
|
+
{ name: "maxTotalSupply", type: "uint256", indexed: false },
|
|
315
|
+
{ name: "annualInterestBips", type: "uint256", indexed: false },
|
|
316
|
+
{ name: "delinquencyFeeBips", type: "uint256", indexed: false },
|
|
317
|
+
{ name: "withdrawalBatchDuration", type: "uint256", indexed: false },
|
|
318
|
+
{ name: "reserveRatioBips", type: "uint256", indexed: false },
|
|
319
|
+
{ name: "delinquencyGracePeriod", type: "uint256", indexed: false },
|
|
320
|
+
{ name: "hooks", type: "uint256", indexed: false }
|
|
321
|
+
]
|
|
322
|
+
}
|
|
323
|
+
];
|
|
324
|
+
var marketAbi = [
|
|
325
|
+
// --- writes ---
|
|
326
|
+
{ type: "function", name: "deposit", inputs: [{ name: "amount", type: "uint256" }], outputs: [], stateMutability: "nonpayable" },
|
|
327
|
+
{ type: "function", name: "depositUpTo", inputs: [{ name: "amount", type: "uint256" }], outputs: [{ type: "uint256" }], stateMutability: "nonpayable" },
|
|
328
|
+
{ type: "function", name: "borrow", inputs: [{ name: "amount", type: "uint256" }], outputs: [], stateMutability: "nonpayable" },
|
|
329
|
+
{ type: "function", name: "repay", inputs: [{ name: "amount", type: "uint256" }], outputs: [], stateMutability: "nonpayable" },
|
|
330
|
+
{
|
|
331
|
+
type: "function",
|
|
332
|
+
name: "repayAndProcessUnpaidWithdrawalBatches",
|
|
333
|
+
inputs: [
|
|
334
|
+
{ name: "repayAmount", type: "uint256" },
|
|
335
|
+
{ name: "maxBatches", type: "uint256" }
|
|
336
|
+
],
|
|
337
|
+
outputs: [],
|
|
338
|
+
stateMutability: "nonpayable"
|
|
339
|
+
},
|
|
340
|
+
{ type: "function", name: "queueWithdrawal", inputs: [{ name: "amount", type: "uint256" }], outputs: [{ name: "expiry", type: "uint32" }], stateMutability: "nonpayable" },
|
|
341
|
+
{ type: "function", name: "queueFullWithdrawal", inputs: [], outputs: [{ name: "expiry", type: "uint32" }], stateMutability: "nonpayable" },
|
|
342
|
+
{
|
|
343
|
+
type: "function",
|
|
344
|
+
name: "executeWithdrawal",
|
|
345
|
+
inputs: [
|
|
346
|
+
{ name: "accountAddress", type: "address" },
|
|
347
|
+
{ name: "expiry", type: "uint32" }
|
|
348
|
+
],
|
|
349
|
+
outputs: [{ type: "uint256" }],
|
|
350
|
+
stateMutability: "nonpayable"
|
|
351
|
+
},
|
|
352
|
+
{ type: "function", name: "closeMarket", inputs: [], outputs: [], stateMutability: "nonpayable" },
|
|
353
|
+
{ type: "function", name: "setBackupCloser", inputs: [{ name: "newBackupCloser", type: "address" }], outputs: [], stateMutability: "nonpayable" },
|
|
354
|
+
{ type: "function", name: "updateState", inputs: [], outputs: [], stateMutability: "nonpayable" },
|
|
355
|
+
// --- reads ---
|
|
356
|
+
{ type: "function", name: "asset", inputs: [], outputs: [{ type: "address" }], stateMutability: "view" },
|
|
357
|
+
{ type: "function", name: "name", inputs: [], outputs: [{ type: "string" }], stateMutability: "view" },
|
|
358
|
+
{ type: "function", name: "symbol", inputs: [], outputs: [{ type: "string" }], stateMutability: "view" },
|
|
359
|
+
{ type: "function", name: "decimals", inputs: [], outputs: [{ type: "uint8" }], stateMutability: "view" },
|
|
360
|
+
{ type: "function", name: "borrower", inputs: [], outputs: [{ type: "address" }], stateMutability: "view" },
|
|
361
|
+
{ type: "function", name: "balanceOf", inputs: [{ name: "account", type: "address" }], outputs: [{ type: "uint256" }], stateMutability: "view" },
|
|
362
|
+
{ type: "function", name: "scaledBalanceOf", inputs: [{ name: "account", type: "address" }], outputs: [{ type: "uint256" }], stateMutability: "view" },
|
|
363
|
+
{ type: "function", name: "totalSupply", inputs: [], outputs: [{ type: "uint256" }], stateMutability: "view" },
|
|
364
|
+
{ type: "function", name: "totalAssets", inputs: [], outputs: [{ type: "uint256" }], stateMutability: "view" },
|
|
365
|
+
{ type: "function", name: "borrowableAssets", inputs: [], outputs: [{ type: "uint256" }], stateMutability: "view" },
|
|
366
|
+
{ type: "function", name: "coverageLiquidity", inputs: [], outputs: [{ type: "uint256" }], stateMutability: "view" },
|
|
367
|
+
{ type: "function", name: "maximumDeposit", inputs: [], outputs: [{ type: "uint256" }], stateMutability: "view" },
|
|
368
|
+
{ type: "function", name: "annualInterestBips", inputs: [], outputs: [{ type: "uint256" }], stateMutability: "view" },
|
|
369
|
+
{ type: "function", name: "reserveRatioBips", inputs: [], outputs: [{ type: "uint256" }], stateMutability: "view" },
|
|
370
|
+
{ type: "function", name: "isClosed", inputs: [], outputs: [{ type: "bool" }], stateMutability: "view" },
|
|
371
|
+
{ type: "function", name: "getUnpaidBatchExpiries", inputs: [], outputs: [{ type: "uint32[]" }], stateMutability: "view" },
|
|
372
|
+
{
|
|
373
|
+
type: "function",
|
|
374
|
+
name: "getAvailableWithdrawalAmount",
|
|
375
|
+
inputs: [
|
|
376
|
+
{ name: "accountAddress", type: "address" },
|
|
377
|
+
{ name: "expiry", type: "uint32" }
|
|
378
|
+
],
|
|
379
|
+
outputs: [{ type: "uint256" }],
|
|
380
|
+
stateMutability: "view"
|
|
381
|
+
},
|
|
382
|
+
// --- events ---
|
|
383
|
+
{ type: "event", name: "Deposit", inputs: [{ name: "account", type: "address", indexed: true }, { name: "assetAmount", type: "uint256", indexed: false }, { name: "scaledAmount", type: "uint256", indexed: false }] },
|
|
384
|
+
{ type: "event", name: "Borrow", inputs: [{ name: "assetAmount", type: "uint256", indexed: false }] },
|
|
385
|
+
{ type: "event", name: "DebtRepaid", inputs: [{ name: "from", type: "address", indexed: true }, { name: "assetAmount", type: "uint256", indexed: false }] },
|
|
386
|
+
{ type: "event", name: "WithdrawalQueued", inputs: [{ name: "expiry", type: "uint32", indexed: true }, { name: "account", type: "address", indexed: true }, { name: "scaledAmount", type: "uint256", indexed: false }, { name: "normalizedAmount", type: "uint256", indexed: false }] },
|
|
387
|
+
{ type: "event", name: "MarketClosed", inputs: [{ name: "timestamp", type: "uint256", indexed: false }] }
|
|
388
|
+
];
|
|
389
|
+
var erc20Abi = [
|
|
390
|
+
{ type: "function", name: "approve", inputs: [{ name: "spender", type: "address" }, { name: "amount", type: "uint256" }], outputs: [{ type: "bool" }], stateMutability: "nonpayable" },
|
|
391
|
+
{ type: "function", name: "allowance", inputs: [{ name: "owner", type: "address" }, { name: "spender", type: "address" }], outputs: [{ type: "uint256" }], stateMutability: "view" },
|
|
392
|
+
{ type: "function", name: "balanceOf", inputs: [{ name: "account", type: "address" }], outputs: [{ type: "uint256" }], stateMutability: "view" },
|
|
393
|
+
{ type: "function", name: "decimals", inputs: [], outputs: [{ type: "uint8" }], stateMutability: "view" },
|
|
394
|
+
{ type: "function", name: "symbol", inputs: [], outputs: [{ type: "string" }], stateMutability: "view" },
|
|
395
|
+
// test-USDC only (permissionless mint on testnet)
|
|
396
|
+
{ type: "function", name: "mint", inputs: [{ name: "to", type: "address" }, { name: "amount", type: "uint256" }], outputs: [], stateMutability: "nonpayable" },
|
|
397
|
+
{ type: "function", name: "drip", inputs: [], outputs: [], stateMutability: "nonpayable" }
|
|
398
|
+
];
|
|
399
|
+
var bondNotesAbi = [
|
|
400
|
+
{ type: "function", name: "describe", inputs: [{ name: "market", type: "address" }, { name: "name", type: "string" }, { name: "description", type: "string" }], outputs: [], stateMutability: "nonpayable" },
|
|
401
|
+
{ type: "function", name: "notes", inputs: [{ name: "market", type: "address" }], outputs: [{ name: "name", type: "string" }, { name: "description", type: "string" }, { name: "author", type: "address" }, { name: "updatedAt", type: "uint40" }], stateMutability: "view" }
|
|
402
|
+
];
|
|
403
|
+
var ERROR_HELP = {
|
|
404
|
+
BorrowerAlreadyExists: "This wallet is already registered as an issuer. You can skip `register` and deploy markets directly.",
|
|
405
|
+
NotApprovedBorrower: "This wallet is not registered as an issuer yet. Run `sbn register` first.",
|
|
406
|
+
HooksTemplateNotFound: "The hooks template address is wrong for this network. Check the deployment addresses.",
|
|
407
|
+
SaltDoesNotContainSender: "The market salt must start with your address. This is an SDK bug \u2014 please report it.",
|
|
408
|
+
MarketAlreadyExists: "A market already exists for that salt. Retry \u2014 the SDK will pick a fresh salt.",
|
|
409
|
+
AssetBlacklisted: "The chosen asset is blacklisted on this deployment.",
|
|
410
|
+
FeeMismatch: "Origination fee args do not match the template (templates are zero-fee; pass 0).",
|
|
411
|
+
DepositToClosedMarket: "This market is closed; deposits are no longer accepted.",
|
|
412
|
+
MaxSupplyExceeded: "Deposit would exceed the market cap (maxTotalSupply). Deposit less or use a market with a higher cap.",
|
|
413
|
+
NullMintAmount: "Deposit too small \u2014 it scales to zero market tokens. Deposit a larger amount.",
|
|
414
|
+
AccountBlocked: "This address is flagged by the sanctions sentinel and cannot transact.",
|
|
415
|
+
BorrowAmountTooHigh: "Borrow exceeds available liquidity (assets minus required reserves and pending withdrawals).",
|
|
416
|
+
BorrowFromClosedMarket: "This market is closed; borrowing is no longer possible.",
|
|
417
|
+
BorrowWhileSanctioned: "The borrower address is flagged by the sanctions sentinel.",
|
|
418
|
+
NullRepayAmount: "Repay amount is zero.",
|
|
419
|
+
RepayToClosedMarket: "This market is closed; use the closure flow instead of repay.",
|
|
420
|
+
CloseMarketWithUnpaidWithdrawals: "Cannot close: there are unpaid withdrawal batches. Repay enough to cover them first.",
|
|
421
|
+
WithdrawalBatchNotExpired: "The withdrawal batch has not expired yet. Wait until the batch expiry before claiming.",
|
|
422
|
+
NullWithdrawalAmount: "Nothing claimable for this account/batch right now.",
|
|
423
|
+
NullBurnAmount: "Withdrawal amount scales to zero \u2014 you may have no balance in this market.",
|
|
424
|
+
CallerNotBorrowerOrBackupCloser: "Only the issuer (borrower) or the configured backup closer can close this market."
|
|
425
|
+
};
|
|
426
|
+
function explainError(err) {
|
|
427
|
+
if (err instanceof BaseError) {
|
|
428
|
+
const revert = err.walk((e) => e instanceof ContractFunctionRevertedError);
|
|
429
|
+
if (revert instanceof ContractFunctionRevertedError) {
|
|
430
|
+
const name = revert.data?.errorName ?? revert.reason ?? "revert";
|
|
431
|
+
const help = ERROR_HELP[name];
|
|
432
|
+
return help ? `${name}: ${help}` : `Reverted: ${name}`;
|
|
433
|
+
}
|
|
434
|
+
return err.shortMessage || err.message;
|
|
435
|
+
}
|
|
436
|
+
if (err instanceof Error) return err.message;
|
|
437
|
+
return String(err);
|
|
438
|
+
}
|
|
439
|
+
var USDC_DECIMALS = 6;
|
|
440
|
+
function pctToBips(pct) {
|
|
441
|
+
return Math.round(pct * 100);
|
|
442
|
+
}
|
|
443
|
+
function parseDuration(input) {
|
|
444
|
+
if (typeof input === "number") return input;
|
|
445
|
+
const m = input.trim().match(/^(\d+(?:\.\d+)?)\s*([smhdwy]?)$/i);
|
|
446
|
+
if (!m) throw new Error(`Invalid duration "${input}". Use e.g. 90d, 1y, 12h, or seconds.`);
|
|
447
|
+
const value = parseFloat(m[1]);
|
|
448
|
+
const unit = (m[2] || "s").toLowerCase();
|
|
449
|
+
const mult = {
|
|
450
|
+
s: 1,
|
|
451
|
+
m: 60,
|
|
452
|
+
h: 3600,
|
|
453
|
+
d: 86400,
|
|
454
|
+
w: 604800,
|
|
455
|
+
y: 31536e3
|
|
456
|
+
};
|
|
457
|
+
return Math.round(value * mult[unit]);
|
|
458
|
+
}
|
|
459
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
460
|
+
function isTransientSendError(err) {
|
|
461
|
+
const msg = (err instanceof Error ? err.message : String(err)).toLowerCase();
|
|
462
|
+
return msg.includes("nonce") || msg.includes("replacement transaction underpriced") || msg.includes("already known") || msg.includes("rate limit") || msg.includes("429") || msg.includes("fetch failed") || msg.includes("failed to fetch") || msg.includes("connection") || msg.includes("timeout");
|
|
463
|
+
}
|
|
464
|
+
async function send(sbn, params) {
|
|
465
|
+
let lastErr;
|
|
466
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
467
|
+
let hash;
|
|
468
|
+
let result;
|
|
469
|
+
try {
|
|
470
|
+
const sim = await sbn.publicClient.simulateContract({
|
|
471
|
+
account: sbn.account,
|
|
472
|
+
address: params.address,
|
|
473
|
+
abi: params.abi,
|
|
474
|
+
functionName: params.functionName,
|
|
475
|
+
args: params.args ?? [],
|
|
476
|
+
value: params.value
|
|
477
|
+
});
|
|
478
|
+
result = sim.result;
|
|
479
|
+
let gas;
|
|
480
|
+
try {
|
|
481
|
+
const est = await sbn.publicClient.estimateContractGas({
|
|
482
|
+
account: sbn.account,
|
|
483
|
+
address: params.address,
|
|
484
|
+
abi: params.abi,
|
|
485
|
+
functionName: params.functionName,
|
|
486
|
+
args: params.args ?? [],
|
|
487
|
+
value: params.value
|
|
488
|
+
});
|
|
489
|
+
gas = est * 160n / 100n;
|
|
490
|
+
} catch {
|
|
491
|
+
gas = void 0;
|
|
492
|
+
}
|
|
493
|
+
hash = await sbn.walletClient.writeContract(
|
|
494
|
+
gas ? { ...sim.request, gas } : sim.request
|
|
495
|
+
);
|
|
496
|
+
} catch (err) {
|
|
497
|
+
lastErr = err;
|
|
498
|
+
if (isTransientSendError(err) && attempt < 3) {
|
|
499
|
+
await sleep(2e3);
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
throw new Error(explainError(err));
|
|
503
|
+
}
|
|
504
|
+
const receipt = await sbn.publicClient.waitForTransactionReceipt({ hash });
|
|
505
|
+
if (receipt.status !== "success") {
|
|
506
|
+
throw new Error(
|
|
507
|
+
`Transaction reverted on-chain (${hash}). This can be an out-of-gas or a state change after simulation \u2014 re-read state with \`sbn status\` and retry.`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
return { hash, result };
|
|
511
|
+
}
|
|
512
|
+
throw new Error(explainError(lastErr));
|
|
513
|
+
}
|
|
514
|
+
async function isRegistered(sbn, address) {
|
|
515
|
+
assertDeployed(sbn.deployment);
|
|
516
|
+
return sbn.publicClient.readContract({
|
|
517
|
+
address: sbn.deployment.contracts.WildcatArchController,
|
|
518
|
+
abi: archControllerAbi,
|
|
519
|
+
functionName: "isRegisteredBorrower",
|
|
520
|
+
args: [address ?? sbn.account.address]
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
async function registerSelf(sbn) {
|
|
524
|
+
assertDeployed(sbn.deployment);
|
|
525
|
+
if (await isRegistered(sbn)) return { already: true };
|
|
526
|
+
const { hash } = await send(sbn, {
|
|
527
|
+
address: sbn.deployment.contracts.WildcatArchController,
|
|
528
|
+
abi: archControllerAbi,
|
|
529
|
+
functionName: "registerSelf"
|
|
530
|
+
});
|
|
531
|
+
await waitForRegistration(sbn);
|
|
532
|
+
return { already: false, hash };
|
|
533
|
+
}
|
|
534
|
+
async function waitForRegistration(sbn, timeoutMs = 6e4) {
|
|
535
|
+
const start = Date.now();
|
|
536
|
+
let delay = 1e3;
|
|
537
|
+
while (Date.now() - start < timeoutMs) {
|
|
538
|
+
if (await isRegistered(sbn)) return;
|
|
539
|
+
await sleep(delay);
|
|
540
|
+
delay = Math.min(delay * 1.5, 5e3);
|
|
541
|
+
}
|
|
542
|
+
if (await isRegistered(sbn)) return;
|
|
543
|
+
throw new Error(
|
|
544
|
+
"Registration did not become visible on the RPC in time. Your wallet is registered on-chain \u2014 wait a few seconds and retry the deploy."
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
function ethBalance(sbn, address) {
|
|
548
|
+
return sbn.publicClient.getBalance({ address: address ?? sbn.account.address });
|
|
549
|
+
}
|
|
550
|
+
function usdcBalance(sbn, address) {
|
|
551
|
+
return sbn.publicClient.readContract({
|
|
552
|
+
address: sbn.deployment.contracts.TestUSDC,
|
|
553
|
+
abi: erc20Abi,
|
|
554
|
+
functionName: "balanceOf",
|
|
555
|
+
args: [address ?? sbn.account.address]
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
async function mintTestUsdc(sbn, amount) {
|
|
559
|
+
assertDeployed(sbn.deployment);
|
|
560
|
+
const { hash } = await send(sbn, {
|
|
561
|
+
address: sbn.deployment.contracts.TestUSDC,
|
|
562
|
+
abi: erc20Abi,
|
|
563
|
+
functionName: "mint",
|
|
564
|
+
args: [sbn.account.address, amount]
|
|
565
|
+
});
|
|
566
|
+
return hash;
|
|
567
|
+
}
|
|
568
|
+
async function waitForEth(sbn, target, timeoutMs = 9e4) {
|
|
569
|
+
const start = Date.now();
|
|
570
|
+
while (Date.now() - start < timeoutMs) {
|
|
571
|
+
if (await ethBalance(sbn) >= target) return true;
|
|
572
|
+
await sleep(3e3);
|
|
573
|
+
}
|
|
574
|
+
return await ethBalance(sbn) >= target;
|
|
575
|
+
}
|
|
576
|
+
function usdcUnits(amount) {
|
|
577
|
+
const clean = String(amount).replace(/[_,\s]/g, "");
|
|
578
|
+
if (!/^\d+(\.\d+)?$/.test(clean)) {
|
|
579
|
+
throw new Error(`Invalid USDC amount "${amount}". Use a positive number like 10000 or 1.5.`);
|
|
580
|
+
}
|
|
581
|
+
const [whole, frac = ""] = clean.split(".");
|
|
582
|
+
const fracPadded = (frac + "0".repeat(USDC_DECIMALS)).slice(0, USDC_DECIMALS);
|
|
583
|
+
const value = BigInt(whole || "0") * 10n ** BigInt(USDC_DECIMALS) + BigInt(fracPadded || "0");
|
|
584
|
+
if (value <= 0n) throw new Error(`USDC amount must be greater than zero (got "${amount}").`);
|
|
585
|
+
return value;
|
|
586
|
+
}
|
|
587
|
+
function randomNonce() {
|
|
588
|
+
return BigInt("0x" + randomBytes(12).toString("hex"));
|
|
589
|
+
}
|
|
590
|
+
function resolveFixedTermEndTime(term) {
|
|
591
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
592
|
+
let end;
|
|
593
|
+
if (typeof term === "number") {
|
|
594
|
+
end = term >= 1e9 ? term : now + term;
|
|
595
|
+
} else {
|
|
596
|
+
const s = term.trim();
|
|
597
|
+
if (/[smhdwy]$/i.test(s)) {
|
|
598
|
+
end = now + parseDuration(s);
|
|
599
|
+
} else if (/^\d+$/.test(s)) {
|
|
600
|
+
const n = Number(s);
|
|
601
|
+
end = n >= 1e9 ? n : now + n;
|
|
602
|
+
} else {
|
|
603
|
+
throw new Error(`Invalid term "${term}". Use a duration like "1y" / "90d", or a unix timestamp.`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (end <= now) throw new Error(`Fixed term must end in the future (resolved to ${end}, now ${now}).`);
|
|
607
|
+
if (end > 4294967295) {
|
|
608
|
+
throw new Error(`Fixed term end ${end} exceeds the uint32 limit (\u2248 year 2106). Choose a shorter term.`);
|
|
609
|
+
}
|
|
610
|
+
return end;
|
|
611
|
+
}
|
|
612
|
+
function computeSalt(borrower, nonce) {
|
|
613
|
+
const addr = BigInt(borrower);
|
|
614
|
+
const salt = addr << 96n | nonce & (1n << 96n) - 1n;
|
|
615
|
+
return `0x${salt.toString(16).padStart(64, "0")}`;
|
|
616
|
+
}
|
|
617
|
+
async function deployMarket(sbn, terms) {
|
|
618
|
+
assertDeployed(sbn.deployment);
|
|
619
|
+
const d = sbn.deployment;
|
|
620
|
+
const type = terms.type ?? "open";
|
|
621
|
+
const finitePct = (v, name2) => {
|
|
622
|
+
if (!Number.isFinite(v) || v < 0) throw new Error(`${name2} must be a non-negative number (got ${v}).`);
|
|
623
|
+
return v;
|
|
624
|
+
};
|
|
625
|
+
const aprPct = finitePct(terms.aprPct ?? 8, "aprPct");
|
|
626
|
+
if (aprPct > 100) {
|
|
627
|
+
throw new Error("aprPct must be \u2264 100. The protocol caps base APR at 100%; use penaltyAprPct for higher delinquency rates.");
|
|
628
|
+
}
|
|
629
|
+
const annualInterestBips = pctToBips(aprPct);
|
|
630
|
+
const delinquencyFeeBips = pctToBips(finitePct(terms.penaltyAprPct ?? 20, "penaltyAprPct"));
|
|
631
|
+
if (delinquencyFeeBips > 65535) throw new Error("penaltyAprPct too high (max ~655%).");
|
|
632
|
+
const reserveRatioBips = pctToBips(finitePct(terms.reservePct ?? 0, "reservePct"));
|
|
633
|
+
if (reserveRatioBips > 1e4) throw new Error("reservePct must be \u2264 100.");
|
|
634
|
+
const withdrawalBatchDuration = parseDuration(terms.withdrawalBatch ?? "1d");
|
|
635
|
+
const delinquencyGracePeriod = parseDuration(terms.gracePeriod ?? "3d");
|
|
636
|
+
const asset = terms.asset ?? d.contracts.TestUSDC;
|
|
637
|
+
const maxTotalSupply = usdcUnits(terms.capUsdc);
|
|
638
|
+
const minimumDeposit = terms.minDepositUsdc ? usdcUnits(terms.minDepositUsdc) : 0n;
|
|
639
|
+
const transfersDisabled = terms.transfersDisabled ?? false;
|
|
640
|
+
const namePrefix = terms.namePrefix ?? "SBN ";
|
|
641
|
+
const symbolPrefix = terms.symbolPrefix ?? "sbn";
|
|
642
|
+
let template;
|
|
643
|
+
let hooksData;
|
|
644
|
+
if (type === "fixed") {
|
|
645
|
+
if (terms.term === void 0) throw new Error('Fixed-term bonds require `term` (e.g. "1y" or a unix timestamp).');
|
|
646
|
+
const fixedTermEndTime = resolveFixedTermEndTime(terms.term);
|
|
647
|
+
template = d.contracts.FixedTermHooksTemplate;
|
|
648
|
+
hooksData = encodeAbiParameters(parseAbiParameters("uint32, uint128, bool, bool, bool"), [
|
|
649
|
+
fixedTermEndTime,
|
|
650
|
+
minimumDeposit,
|
|
651
|
+
transfersDisabled,
|
|
652
|
+
terms.allowClosureBeforeTerm ?? true,
|
|
653
|
+
terms.allowTermReduction ?? true
|
|
654
|
+
]);
|
|
655
|
+
} else {
|
|
656
|
+
template = d.contracts.OpenTermHooksTemplate;
|
|
657
|
+
hooksData = encodeAbiParameters(parseAbiParameters("uint128, bool"), [minimumDeposit, transfersDisabled]);
|
|
658
|
+
}
|
|
659
|
+
const params = {
|
|
660
|
+
asset,
|
|
661
|
+
namePrefix,
|
|
662
|
+
symbolPrefix,
|
|
663
|
+
maxTotalSupply,
|
|
664
|
+
annualInterestBips,
|
|
665
|
+
delinquencyFeeBips,
|
|
666
|
+
withdrawalBatchDuration,
|
|
667
|
+
reserveRatioBips,
|
|
668
|
+
delinquencyGracePeriod,
|
|
669
|
+
hooks: 0n
|
|
670
|
+
// factory injects the hooks instance address; template sets flags
|
|
671
|
+
};
|
|
672
|
+
const salt = computeSalt(sbn.account.address, randomNonce());
|
|
673
|
+
const deployArgs = [template, "0x", params, hooksData, salt, zeroAddress, 0n];
|
|
674
|
+
let sent;
|
|
675
|
+
let lastErr;
|
|
676
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
677
|
+
try {
|
|
678
|
+
sent = await send(sbn, {
|
|
679
|
+
address: d.contracts.HooksFactory,
|
|
680
|
+
abi: hooksFactoryAbi,
|
|
681
|
+
functionName: "deployMarketAndHooks",
|
|
682
|
+
args: deployArgs
|
|
683
|
+
});
|
|
684
|
+
break;
|
|
685
|
+
} catch (err) {
|
|
686
|
+
lastErr = err;
|
|
687
|
+
if (!await isRegistered(sbn)) throw err;
|
|
688
|
+
await sleep(2500);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (!sent) throw new Error(explainError(lastErr));
|
|
692
|
+
const { hash, result } = sent;
|
|
693
|
+
const [market, hooksInstance] = result;
|
|
694
|
+
const readWithRetry = async (fn) => {
|
|
695
|
+
let lastErr2;
|
|
696
|
+
for (let i = 0; i < 6; i++) {
|
|
697
|
+
try {
|
|
698
|
+
return await sbn.publicClient.readContract({ address: market, abi: marketAbi, functionName: fn });
|
|
699
|
+
} catch (err) {
|
|
700
|
+
lastErr2 = err;
|
|
701
|
+
await sleep(1500);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
throw lastErr2;
|
|
705
|
+
};
|
|
706
|
+
const [name, symbol] = await Promise.all([readWithRetry("name"), readWithRetry("symbol")]);
|
|
707
|
+
const record = {
|
|
708
|
+
market,
|
|
709
|
+
hooksInstance,
|
|
710
|
+
type,
|
|
711
|
+
asset,
|
|
712
|
+
name,
|
|
713
|
+
symbol,
|
|
714
|
+
network: d.chain,
|
|
715
|
+
txHash: hash,
|
|
716
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
717
|
+
};
|
|
718
|
+
recordMarket(record);
|
|
719
|
+
return {
|
|
720
|
+
market,
|
|
721
|
+
hooksInstance,
|
|
722
|
+
hash,
|
|
723
|
+
name,
|
|
724
|
+
symbol,
|
|
725
|
+
type,
|
|
726
|
+
explorerUrl: addressUrl(d, market),
|
|
727
|
+
txUrl: txUrl(d, hash)
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
async function approveIfNeeded(sbn, token, spender, amount) {
|
|
731
|
+
const readAllowance = () => sbn.publicClient.readContract({
|
|
732
|
+
address: token,
|
|
733
|
+
abi: erc20Abi,
|
|
734
|
+
functionName: "allowance",
|
|
735
|
+
args: [sbn.account.address, spender]
|
|
736
|
+
});
|
|
737
|
+
if (await readAllowance() >= amount) return;
|
|
738
|
+
await send(sbn, {
|
|
739
|
+
address: token,
|
|
740
|
+
abi: erc20Abi,
|
|
741
|
+
functionName: "approve",
|
|
742
|
+
args: [spender, 2n ** 256n - 1n]
|
|
743
|
+
});
|
|
744
|
+
const start = Date.now();
|
|
745
|
+
let delay = 1500;
|
|
746
|
+
while (Date.now() - start < 3e4) {
|
|
747
|
+
if (await readAllowance() >= amount) return;
|
|
748
|
+
await sleep(delay);
|
|
749
|
+
delay = Math.min(delay * 1.5, 5e3);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
async function marketAsset(sbn, market) {
|
|
753
|
+
return sbn.publicClient.readContract({ address: market, abi: marketAbi, functionName: "asset" });
|
|
754
|
+
}
|
|
755
|
+
async function deposit(sbn, market, amountUsdc) {
|
|
756
|
+
const amount = usdcUnits(amountUsdc);
|
|
757
|
+
const asset = await marketAsset(sbn, market);
|
|
758
|
+
await approveIfNeeded(sbn, asset, market, amount);
|
|
759
|
+
const { hash } = await send(sbn, { address: market, abi: marketAbi, functionName: "deposit", args: [amount] });
|
|
760
|
+
return hash;
|
|
761
|
+
}
|
|
762
|
+
async function borrow(sbn, market, amountUsdc) {
|
|
763
|
+
const { hash } = await send(sbn, { address: market, abi: marketAbi, functionName: "borrow", args: [usdcUnits(amountUsdc)] });
|
|
764
|
+
return hash;
|
|
765
|
+
}
|
|
766
|
+
async function repay(sbn, market, amountUsdc) {
|
|
767
|
+
const amount = usdcUnits(amountUsdc);
|
|
768
|
+
const asset = await marketAsset(sbn, market);
|
|
769
|
+
await approveIfNeeded(sbn, asset, market, amount);
|
|
770
|
+
const { hash } = await send(sbn, { address: market, abi: marketAbi, functionName: "repay", args: [amount] });
|
|
771
|
+
return hash;
|
|
772
|
+
}
|
|
773
|
+
async function queueWithdrawal(sbn, market, amountUsdc) {
|
|
774
|
+
if (amountUsdc === void 0) {
|
|
775
|
+
const { hash: hash2, result: result2 } = await send(sbn, { address: market, abi: marketAbi, functionName: "queueFullWithdrawal" });
|
|
776
|
+
return { hash: hash2, expiry: Number(result2) };
|
|
777
|
+
}
|
|
778
|
+
const { hash, result } = await send(sbn, {
|
|
779
|
+
address: market,
|
|
780
|
+
abi: marketAbi,
|
|
781
|
+
functionName: "queueWithdrawal",
|
|
782
|
+
args: [usdcUnits(amountUsdc)]
|
|
783
|
+
});
|
|
784
|
+
return { hash, expiry: Number(result) };
|
|
785
|
+
}
|
|
786
|
+
async function executeWithdrawal(sbn, market, account2, expiry) {
|
|
787
|
+
const { hash } = await send(sbn, {
|
|
788
|
+
address: market,
|
|
789
|
+
abi: marketAbi,
|
|
790
|
+
functionName: "executeWithdrawal",
|
|
791
|
+
args: [account2, expiry]
|
|
792
|
+
});
|
|
793
|
+
return hash;
|
|
794
|
+
}
|
|
795
|
+
async function closeMarket(sbn, market) {
|
|
796
|
+
const { hash } = await send(sbn, { address: market, abi: marketAbi, functionName: "closeMarket" });
|
|
797
|
+
return hash;
|
|
798
|
+
}
|
|
799
|
+
async function describeBond(sbn, market, name, description) {
|
|
800
|
+
const registry = sbn.deployment.contracts.BondNotes;
|
|
801
|
+
if (!registry) {
|
|
802
|
+
throw new Error(
|
|
803
|
+
`Descriptions aren't available on ${sbn.deployment.label} yet (no BondNotes registry).`
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
const { hash } = await send(sbn, {
|
|
807
|
+
address: registry,
|
|
808
|
+
abi: bondNotesAbi,
|
|
809
|
+
functionName: "describe",
|
|
810
|
+
args: [market, name ?? "", description ?? ""]
|
|
811
|
+
});
|
|
812
|
+
return hash;
|
|
813
|
+
}
|
|
814
|
+
async function marketStatus(sbn, market) {
|
|
815
|
+
const read = (functionName, args = []) => sbn.publicClient.readContract({ address: market, abi: marketAbi, functionName, args });
|
|
816
|
+
const [
|
|
817
|
+
name,
|
|
818
|
+
symbol,
|
|
819
|
+
asset,
|
|
820
|
+
borrower,
|
|
821
|
+
isClosed,
|
|
822
|
+
apr,
|
|
823
|
+
reserve,
|
|
824
|
+
totalSupply,
|
|
825
|
+
totalAssets,
|
|
826
|
+
borrowableAssets,
|
|
827
|
+
maximumDeposit,
|
|
828
|
+
expiries
|
|
829
|
+
] = await Promise.all([
|
|
830
|
+
read("name"),
|
|
831
|
+
read("symbol"),
|
|
832
|
+
read("asset"),
|
|
833
|
+
read("borrower"),
|
|
834
|
+
read("isClosed"),
|
|
835
|
+
read("annualInterestBips"),
|
|
836
|
+
read("reserveRatioBips"),
|
|
837
|
+
read("totalSupply"),
|
|
838
|
+
read("totalAssets"),
|
|
839
|
+
read("borrowableAssets"),
|
|
840
|
+
read("maximumDeposit"),
|
|
841
|
+
read("getUnpaidBatchExpiries")
|
|
842
|
+
]);
|
|
843
|
+
return {
|
|
844
|
+
market,
|
|
845
|
+
name,
|
|
846
|
+
symbol,
|
|
847
|
+
asset,
|
|
848
|
+
borrower,
|
|
849
|
+
isClosed,
|
|
850
|
+
annualInterestBips: Number(apr),
|
|
851
|
+
reserveRatioBips: Number(reserve),
|
|
852
|
+
maxTotalSupply: totalSupply + maximumDeposit,
|
|
853
|
+
totalSupply,
|
|
854
|
+
totalAssets,
|
|
855
|
+
borrowableAssets,
|
|
856
|
+
maximumDeposit,
|
|
857
|
+
unpaidBatchExpiries: expiries.map(Number)
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
var noop = () => {
|
|
861
|
+
};
|
|
862
|
+
var DEFAULT_MIN_ETH = parseEther("0.001");
|
|
863
|
+
var DEFAULT_MINT_USDC = 1000000n * 10n ** 6n;
|
|
864
|
+
async function ensureFunded(sbn, opts = {}) {
|
|
865
|
+
assertDeployed(sbn.deployment);
|
|
866
|
+
const log = opts.log ?? noop;
|
|
867
|
+
const minEth = opts.minEthWei ?? DEFAULT_MIN_ETH;
|
|
868
|
+
const mintTarget = opts.mintUsdc ?? DEFAULT_MINT_USDC;
|
|
869
|
+
const ethBefore = await ethBalance(sbn);
|
|
870
|
+
if (!sbn.deployment.testnet) {
|
|
871
|
+
if (ethBefore < minEth) {
|
|
872
|
+
throw new Error(
|
|
873
|
+
`Wallet ${sbn.account.address} has too little ETH for gas on ${sbn.deployment.label}. There is no dispenser on mainnet \u2014 send Base ETH to this address and retry. Issuing a bond needs only gas; lending also needs real USDC.`
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
return {
|
|
877
|
+
ethBefore,
|
|
878
|
+
ethAfter: ethBefore,
|
|
879
|
+
usdcAfter: await usdcBalance(sbn),
|
|
880
|
+
faucetUsed: false
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
let faucetUsed = false;
|
|
884
|
+
let faucetMessage;
|
|
885
|
+
if (ethBefore < minEth) {
|
|
886
|
+
log(`Requesting gas from the sellbonds.now dispenser (${sbn.deployment.faucetUrl})\u2026`);
|
|
887
|
+
const res = await requestEthFromFaucet(sbn.deployment.faucetUrl, sbn.account.address);
|
|
888
|
+
faucetUsed = true;
|
|
889
|
+
faucetMessage = res.message;
|
|
890
|
+
if (!res.ok) {
|
|
891
|
+
throw new Error(
|
|
892
|
+
`Dispenser could not fund ${sbn.account.address}: ${res.message ?? "unknown error"}. Fund it manually with Base Sepolia ETH (https://portal.cdp.coinbase.com/products/faucet) and retry.`
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
log("Waiting for gas to arrive\u2026");
|
|
896
|
+
const arrived = await waitForEth(sbn, minEth);
|
|
897
|
+
if (!arrived) {
|
|
898
|
+
throw new Error(
|
|
899
|
+
`Gas did not arrive in time. Check ${sbn.account.address} on the explorer and retry once it has ETH.`
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
const usdcNow = await usdcBalance(sbn);
|
|
904
|
+
if (usdcNow < mintTarget) {
|
|
905
|
+
log("Minting test USDC\u2026");
|
|
906
|
+
await mintTestUsdc(sbn, mintTarget - usdcNow);
|
|
907
|
+
}
|
|
908
|
+
return {
|
|
909
|
+
ethBefore,
|
|
910
|
+
ethAfter: await ethBalance(sbn),
|
|
911
|
+
usdcAfter: await usdcBalance(sbn),
|
|
912
|
+
faucetUsed,
|
|
913
|
+
faucetMessage
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// src/index.ts
|
|
918
|
+
var SITE = "https://sellbonds.now";
|
|
919
|
+
var server = new McpServer({ name: "sellbonds", version: "0.1.0" });
|
|
920
|
+
var toJson = (v) => JSON.stringify(v, (_k, val) => typeof val === "bigint" ? val.toString() : val, 2);
|
|
921
|
+
var ok = (v) => ({ content: [{ type: "text", text: typeof v === "string" ? v : toJson(v) }] });
|
|
922
|
+
var fail = (msg) => ({ isError: true, content: [{ type: "text", text: msg }] });
|
|
923
|
+
var wrap = (fn) => async (args) => {
|
|
924
|
+
try {
|
|
925
|
+
return ok(await fn(args));
|
|
926
|
+
} catch (e) {
|
|
927
|
+
return fail(e instanceof Error ? e.message : String(e));
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
var networkParam = z.enum(["base", "base-sepolia"]).optional().describe("Network. Defaults to SBN_NETWORK or base (mainnet, real USDC). base-sepolia is the free testnet.");
|
|
931
|
+
var marketParam = z.string().regex(/^0x[0-9a-fA-F]{40}$/).describe("Bond market contract address (0x\u2026)");
|
|
932
|
+
function isMainnet(network) {
|
|
933
|
+
return (network ?? networkName()) !== "base-sepolia";
|
|
934
|
+
}
|
|
935
|
+
var RISK_GATE = `This would create a new financial commitment with real USDC on Base mainnet. Before retrying with acknowledge_risks=true, confirm with your human that they understand and accept: (1) bonds are uncollateralized \u2014 a lender can lose 100%; (2) the contracts are an unaudited fork of Wildcat V2; (3) transactions are irreversible; (4) there is no KYC/sanctions layer \u2014 legal/tax/regulatory compliance is their responsibility. Full text: ${SITE}/risk.md`;
|
|
936
|
+
server.registerTool(
|
|
937
|
+
"list_bonds",
|
|
938
|
+
{
|
|
939
|
+
title: "List all bonds",
|
|
940
|
+
description: "Every bond issued on sellbonds.now (Base mainnet), read live from the on-chain registry: terms, APR, raised/cap, status, issuer, plus network aggregates.",
|
|
941
|
+
inputSchema: {}
|
|
942
|
+
},
|
|
943
|
+
wrap(async () => {
|
|
944
|
+
const res = await fetch(`${SITE}/api/markets`);
|
|
945
|
+
if (!res.ok) throw new Error(`markets API ${res.status}`);
|
|
946
|
+
return res.json();
|
|
947
|
+
})
|
|
948
|
+
);
|
|
949
|
+
server.registerTool(
|
|
950
|
+
"get_bond",
|
|
951
|
+
{
|
|
952
|
+
title: "Get one bond",
|
|
953
|
+
description: "Full detail for one bond on Base mainnet: live state, terms, issuer, and its on-chain event history (investments, draws, repayments).",
|
|
954
|
+
inputSchema: { market: marketParam }
|
|
955
|
+
},
|
|
956
|
+
wrap(async ({ market }) => {
|
|
957
|
+
const res = await fetch(`${SITE}/api/bond?market=${market}`);
|
|
958
|
+
if (res.status === 404) throw new Error("No sellbonds bond at that address.");
|
|
959
|
+
if (!res.ok) throw new Error(`bond API ${res.status}`);
|
|
960
|
+
return res.json();
|
|
961
|
+
})
|
|
962
|
+
);
|
|
963
|
+
server.registerTool(
|
|
964
|
+
"bond_status",
|
|
965
|
+
{
|
|
966
|
+
title: "Read bond state from chain",
|
|
967
|
+
description: "Read a bond market directly from the chain via RPC (works on testnet too, unlike get_bond). Returns raw contract state.",
|
|
968
|
+
inputSchema: { market: marketParam, network: networkParam }
|
|
969
|
+
},
|
|
970
|
+
wrap(async ({ market, network }) => {
|
|
971
|
+
const sbn = connectReadonly(network);
|
|
972
|
+
const status = await marketStatus(sbn, market);
|
|
973
|
+
return { network: network ?? networkName(), ...status, explorerUrl: addressUrl(sbn.deployment, market) };
|
|
974
|
+
})
|
|
975
|
+
);
|
|
976
|
+
server.registerTool(
|
|
977
|
+
"wallet_status",
|
|
978
|
+
{
|
|
979
|
+
title: "Wallet status",
|
|
980
|
+
description: "The local sellbonds wallet (shared with the sbn CLI at ~/.sellbondsnow/wallet.json): address, balances, issuer registration. Set create=true to create one if it doesn't exist.",
|
|
981
|
+
inputSchema: {
|
|
982
|
+
network: networkParam,
|
|
983
|
+
create: z.boolean().optional().describe("Create the wallet if none exists (default false)")
|
|
984
|
+
}
|
|
985
|
+
},
|
|
986
|
+
wrap(async ({ network, create }) => {
|
|
987
|
+
if (!walletExists() && !create) {
|
|
988
|
+
return { exists: false, hint: "No wallet yet. Call wallet_status with create=true to create one (key stays local)." };
|
|
989
|
+
}
|
|
990
|
+
const { wallet, created } = create ? loadOrCreateWallet() : { wallet: loadWallet(), created: false };
|
|
991
|
+
const sbn = connect(network);
|
|
992
|
+
const [eth, usdc, registered] = await Promise.all([
|
|
993
|
+
ethBalance(sbn),
|
|
994
|
+
usdcBalance(sbn),
|
|
995
|
+
isRegistered(sbn)
|
|
996
|
+
]);
|
|
997
|
+
return {
|
|
998
|
+
exists: true,
|
|
999
|
+
created,
|
|
1000
|
+
address: wallet.address,
|
|
1001
|
+
network: network ?? networkName(),
|
|
1002
|
+
ethWei: eth,
|
|
1003
|
+
usdc: Number(usdc) / 1e6,
|
|
1004
|
+
registeredAsIssuer: registered,
|
|
1005
|
+
explorerUrl: addressUrl(sbn.deployment, wallet.address),
|
|
1006
|
+
myBonds: listMarkets()
|
|
1007
|
+
};
|
|
1008
|
+
})
|
|
1009
|
+
);
|
|
1010
|
+
server.registerTool(
|
|
1011
|
+
"fund_wallet_testnet",
|
|
1012
|
+
{
|
|
1013
|
+
title: "Fund wallet (testnet only)",
|
|
1014
|
+
description: "Top up the local wallet with free Base Sepolia gas (via the sellbonds.now dispenser) and test USDC. Testnet only \u2014 on mainnet the wallet must be funded externally.",
|
|
1015
|
+
inputSchema: {}
|
|
1016
|
+
},
|
|
1017
|
+
wrap(async () => {
|
|
1018
|
+
loadOrCreateWallet();
|
|
1019
|
+
const sbn = connect("base-sepolia");
|
|
1020
|
+
const result = await ensureFunded(sbn);
|
|
1021
|
+
return { address: sbn.account.address, ...result };
|
|
1022
|
+
})
|
|
1023
|
+
);
|
|
1024
|
+
server.registerTool(
|
|
1025
|
+
"issue_bond",
|
|
1026
|
+
{
|
|
1027
|
+
title: "Issue a bond (raise)",
|
|
1028
|
+
description: "One-shot raise: ensures the wallet is funded (testnet) and registered as an issuer, deploys a bond market with your terms, and sets its on-chain name/description. ALWAYS set an honest name + description \u2014 unnamed bonds get skipped by lenders. On mainnet requires acknowledge_risks=true after the human has confirmed the risks.",
|
|
1029
|
+
inputSchema: {
|
|
1030
|
+
capUsdc: z.number().positive().describe("Max capital to raise, in USDC (e.g. 10000)"),
|
|
1031
|
+
aprPct: z.number().min(0).max(100).optional().describe("Annual coupon for lenders, percent (e.g. 8.5)"),
|
|
1032
|
+
type: z.enum(["open", "fixed"]).optional().describe("'open' (revolving, default) or 'fixed' (matures)"),
|
|
1033
|
+
term: z.string().optional().describe("Fixed-term duration, e.g. '90d' or '1y' (fixed type only)"),
|
|
1034
|
+
name: z.string().max(80).optional().describe('Short honest label, e.g. "GPU cluster Q3"'),
|
|
1035
|
+
description: z.string().max(500).optional().describe("What the money funds \u2014 honest, no invented returns"),
|
|
1036
|
+
network: networkParam,
|
|
1037
|
+
acknowledge_risks: z.boolean().optional().describe("Required true on mainnet, only after human confirmation")
|
|
1038
|
+
}
|
|
1039
|
+
},
|
|
1040
|
+
wrap(
|
|
1041
|
+
async (a) => {
|
|
1042
|
+
if (isMainnet(a.network) && !a.acknowledge_risks) throw new Error(RISK_GATE);
|
|
1043
|
+
loadOrCreateWallet();
|
|
1044
|
+
const sbn = connect(a.network);
|
|
1045
|
+
await ensureFunded(sbn);
|
|
1046
|
+
await registerSelf(sbn);
|
|
1047
|
+
const market = await deployMarket(sbn, {
|
|
1048
|
+
type: a.type ?? "open",
|
|
1049
|
+
capUsdc: a.capUsdc,
|
|
1050
|
+
aprPct: a.aprPct,
|
|
1051
|
+
namePrefix: a.name,
|
|
1052
|
+
term: a.term
|
|
1053
|
+
});
|
|
1054
|
+
let noteSet = false;
|
|
1055
|
+
if (a.name || a.description) {
|
|
1056
|
+
try {
|
|
1057
|
+
await describeBond(sbn, market.market, a.name ?? "", a.description ?? "");
|
|
1058
|
+
noteSet = true;
|
|
1059
|
+
} catch {
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
return {
|
|
1063
|
+
market: market.market,
|
|
1064
|
+
name: market.name,
|
|
1065
|
+
symbol: market.symbol,
|
|
1066
|
+
type: market.type,
|
|
1067
|
+
issuer: sbn.account.address,
|
|
1068
|
+
explorerUrl: market.explorerUrl,
|
|
1069
|
+
deployTxUrl: market.txUrl,
|
|
1070
|
+
bondPage: `${SITE}/bond/${String(market.market).toLowerCase()}`,
|
|
1071
|
+
noteSet,
|
|
1072
|
+
next: "Lenders fund it with the deposit tool (or: sbn deposit <market> <usdc>). Draw capital with borrow; repay with repay."
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
)
|
|
1076
|
+
);
|
|
1077
|
+
server.registerTool(
|
|
1078
|
+
"deposit",
|
|
1079
|
+
{
|
|
1080
|
+
title: "Fund a bond (lend)",
|
|
1081
|
+
description: "Lend USDC into a bond and receive its interest-accruing bond token. Uncollateralized \u2014 you can lose everything if the issuer defaults. On mainnet requires acknowledge_risks=true after the human has confirmed the risks.",
|
|
1082
|
+
inputSchema: {
|
|
1083
|
+
market: marketParam,
|
|
1084
|
+
amountUsdc: z.number().positive().describe("USDC to lend (human units)"),
|
|
1085
|
+
network: networkParam,
|
|
1086
|
+
acknowledge_risks: z.boolean().optional().describe("Required true on mainnet, only after human confirmation")
|
|
1087
|
+
}
|
|
1088
|
+
},
|
|
1089
|
+
wrap(async (a) => {
|
|
1090
|
+
if (isMainnet(a.network) && !a.acknowledge_risks) throw new Error(RISK_GATE);
|
|
1091
|
+
const sbn = connect(a.network);
|
|
1092
|
+
const hash = await deposit(sbn, a.market, a.amountUsdc);
|
|
1093
|
+
return { txHash: hash, txUrl: txUrl(sbn.deployment, hash), deposited: a.amountUsdc, market: a.market };
|
|
1094
|
+
})
|
|
1095
|
+
);
|
|
1096
|
+
server.registerTool(
|
|
1097
|
+
"borrow",
|
|
1098
|
+
{
|
|
1099
|
+
title: "Draw down raised capital (issuer)",
|
|
1100
|
+
description: "Move raised USDC from your bond market to your wallet, leaving the reserve ratio behind.",
|
|
1101
|
+
inputSchema: { market: marketParam, amountUsdc: z.number().positive(), network: networkParam }
|
|
1102
|
+
},
|
|
1103
|
+
wrap(async (a) => {
|
|
1104
|
+
const sbn = connect(a.network);
|
|
1105
|
+
const hash = await borrow(sbn, a.market, a.amountUsdc);
|
|
1106
|
+
return { txHash: hash, txUrl: txUrl(sbn.deployment, hash), borrowed: a.amountUsdc, market: a.market };
|
|
1107
|
+
})
|
|
1108
|
+
);
|
|
1109
|
+
server.registerTool(
|
|
1110
|
+
"repay",
|
|
1111
|
+
{
|
|
1112
|
+
title: "Repay a bond (issuer)",
|
|
1113
|
+
description: "Return principal + interest to the bond market. On-time repayment builds your on-chain credit record.",
|
|
1114
|
+
inputSchema: { market: marketParam, amountUsdc: z.number().positive(), network: networkParam }
|
|
1115
|
+
},
|
|
1116
|
+
wrap(async (a) => {
|
|
1117
|
+
const sbn = connect(a.network);
|
|
1118
|
+
const hash = await repay(sbn, a.market, a.amountUsdc);
|
|
1119
|
+
return { txHash: hash, txUrl: txUrl(sbn.deployment, hash), repaid: a.amountUsdc, market: a.market };
|
|
1120
|
+
})
|
|
1121
|
+
);
|
|
1122
|
+
server.registerTool(
|
|
1123
|
+
"withdraw",
|
|
1124
|
+
{
|
|
1125
|
+
title: "Queue a withdrawal (lender)",
|
|
1126
|
+
description: "Queue redemption of your bond tokens (all of them if amountUsdc is omitted). Returns the batch expiry \u2014 claim after it passes with the claim tool.",
|
|
1127
|
+
inputSchema: { market: marketParam, amountUsdc: z.number().positive().optional(), network: networkParam }
|
|
1128
|
+
},
|
|
1129
|
+
wrap(async (a) => {
|
|
1130
|
+
const sbn = connect(a.network);
|
|
1131
|
+
const { hash, expiry } = await queueWithdrawal(sbn, a.market, a.amountUsdc);
|
|
1132
|
+
return { txHash: hash, txUrl: txUrl(sbn.deployment, hash), expiry, next: `After expiry ${expiry}, call claim with this market + expiry.` };
|
|
1133
|
+
})
|
|
1134
|
+
);
|
|
1135
|
+
server.registerTool(
|
|
1136
|
+
"claim",
|
|
1137
|
+
{
|
|
1138
|
+
title: "Claim a matured withdrawal (lender)",
|
|
1139
|
+
description: "Execute a withdrawal batch after its expiry to receive USDC.",
|
|
1140
|
+
inputSchema: { market: marketParam, expiry: z.number().int().describe("Batch expiry from the withdraw tool"), network: networkParam }
|
|
1141
|
+
},
|
|
1142
|
+
wrap(async (a) => {
|
|
1143
|
+
const sbn = connect(a.network);
|
|
1144
|
+
const hash = await executeWithdrawal(sbn, a.market, sbn.account.address, a.expiry);
|
|
1145
|
+
return { txHash: hash, txUrl: txUrl(sbn.deployment, hash) };
|
|
1146
|
+
})
|
|
1147
|
+
);
|
|
1148
|
+
server.registerTool(
|
|
1149
|
+
"close_bond",
|
|
1150
|
+
{
|
|
1151
|
+
title: "Close a bond (issuer)",
|
|
1152
|
+
description: "Settle and close a bond market: repays outstanding debt from your wallet and ends the bond.",
|
|
1153
|
+
inputSchema: { market: marketParam, network: networkParam }
|
|
1154
|
+
},
|
|
1155
|
+
wrap(async (a) => {
|
|
1156
|
+
const sbn = connect(a.network);
|
|
1157
|
+
const hash = await closeMarket(sbn, a.market);
|
|
1158
|
+
return { txHash: hash, txUrl: txUrl(sbn.deployment, hash), closed: a.market };
|
|
1159
|
+
})
|
|
1160
|
+
);
|
|
1161
|
+
server.registerTool(
|
|
1162
|
+
"describe_bond",
|
|
1163
|
+
{
|
|
1164
|
+
title: "Name/describe a bond (issuer)",
|
|
1165
|
+
description: "Set or update the on-chain name + description of a bond you issued. Honest labels matter \u2014 unnamed bonds get skipped by lenders.",
|
|
1166
|
+
inputSchema: {
|
|
1167
|
+
market: marketParam,
|
|
1168
|
+
name: z.string().max(80).describe('Short label, e.g. "GPU cluster Q3"'),
|
|
1169
|
+
description: z.string().max(500).describe("What the money funds"),
|
|
1170
|
+
network: networkParam
|
|
1171
|
+
}
|
|
1172
|
+
},
|
|
1173
|
+
wrap(async (a) => {
|
|
1174
|
+
const sbn = connect(a.network);
|
|
1175
|
+
await describeBond(sbn, a.market, a.name, a.description);
|
|
1176
|
+
return { market: a.market, name: a.name, description: a.description, bondPage: `${SITE}/bond/${a.market.toLowerCase()}` };
|
|
1177
|
+
})
|
|
1178
|
+
);
|
|
1179
|
+
var transport = new StdioServerTransport();
|
|
1180
|
+
await server.connect(transport);
|
|
1181
|
+
console.error(`sellbonds-mcp ready (network default: ${networkName()}) \u2014 docs: ${SITE}/llms.txt`);
|