paralith-mcp 0.4.1 → 0.4.2

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/graph.json CHANGED
@@ -1,37 +1,37 @@
1
- {
2
- "node": "https://hyperbeam.tylerw.ai",
3
- "owner": "DA9qhP25ZPz6MHIhO-7aNHDN3LsTAL7yCKYIkqr13Z8",
4
- "deployedAt": "2026-10-12T12:57:04.194Z",
5
- "processes": {
6
- "game": "AUitn972WGLDj0pMCVbXXacTtOk2PNchgP2qaP_kNoI",
7
- "rune": "rYfYbQu9vIin_FKcpkxLtcF21pIQbdY-pOkkM5igyVg",
8
- "pusd": "N4VZxxFb4qArB4SJawvqnLZSRPX9GFs7iGnRQ3Kr-4M",
9
- "dao": "YAPCQK-3STKc-XND5-O4v-wEQBv7M4S-i__rkQKvQdk",
10
- "elements": {
11
- "earth": "wCtjJMScFI6jjioNQdRDUdyFWiqYubaRYyUeeMYYOjI",
12
- "air": "1he9rnfufmEoPFeDYV_LzkA7e3AM98MXARal2Oq-4lc",
13
- "fire": "4lffB0KjBEix1I7g1nBmeIdllhqhyHH75NiUQs5De2o",
14
- "water": "EwiZp-92W7k8xZntMs2u3HAJ-RdDqTdAzvia_6KxlI0"
15
- },
16
- "internalVenue": "_ShRHGFliguTz51f7N_j69HDdgt-AI7U1LNG6c_uvwc",
17
- "externalVenue": "DhI-4XuwkVxfbzp2tk7PgclUD7lVOJa-kNF2VVhLyDk",
18
- "hunt": "kKpOdP--5hA3fy-i5q6-agoHFNKuYW4c_9Qg8NvAIew",
19
- "huntWorkers": [
20
- "kKpOdP--5hA3fy-i5q6-agoHFNKuYW4c_9Qg8NvAIew",
21
- "NsIK9D0VScuNdf7-avycKxPSTEiHXPBlbFuCEv5ZEuw",
22
- "JYtYuPA5weAbXykDnmDUhlU3LacxkY15qWppEDtTP4s",
23
- "lJgsZ3GadofVafa8FI3C6fGTf6Aq0yfZFi44P6vRti0",
24
- "GUMGAcSdCmKhb75M3NHUkLsdyoAHu7ixyuEldDsXA3c"
25
- ],
26
- "battleWorkers": [
27
- "MgxNZ60Ejc01E5MeuZ1s_8PeLNhExjOz9wUGo3utE1w",
28
- "Ww_GXme0Sn3-jIcji0dPxV0nXLYqBesChXqqXQTC0Hc",
29
- "4YYgYXTzS_PjbQMthDwf93zik-kvoXYTzrNHA30ijAY",
30
- "EWlpRzX-waNNKj0bvT9pzj_N4_bv2ndYPMOJaiVkWlw",
31
- "v5_n4eOw24s1wGjI8-fjoGTfQHwjcO-tcHFxIiO5Qrk"
32
- ]
33
- },
34
- "huntNode": "https://hyperbeam.tylerw.ai",
35
- "marketNode": "https://hyperbeam.tylerw.ai",
36
- "venueNode": "https://hyperbeam.tylerw.ai"
37
- }
1
+ {
2
+ "node": "https://hyperbeam.tylerw.ai",
3
+ "owner": "DA9qhP25ZPz6MHIhO-7aNHDN3LsTAL7yCKYIkqr13Z8",
4
+ "deployedAt": "2026-10-12T12:57:04.194Z",
5
+ "processes": {
6
+ "game": "AUitn972WGLDj0pMCVbXXacTtOk2PNchgP2qaP_kNoI",
7
+ "rune": "rYfYbQu9vIin_FKcpkxLtcF21pIQbdY-pOkkM5igyVg",
8
+ "pusd": "N4VZxxFb4qArB4SJawvqnLZSRPX9GFs7iGnRQ3Kr-4M",
9
+ "dao": "YAPCQK-3STKc-XND5-O4v-wEQBv7M4S-i__rkQKvQdk",
10
+ "elements": {
11
+ "earth": "wCtjJMScFI6jjioNQdRDUdyFWiqYubaRYyUeeMYYOjI",
12
+ "air": "1he9rnfufmEoPFeDYV_LzkA7e3AM98MXARal2Oq-4lc",
13
+ "fire": "4lffB0KjBEix1I7g1nBmeIdllhqhyHH75NiUQs5De2o",
14
+ "water": "EwiZp-92W7k8xZntMs2u3HAJ-RdDqTdAzvia_6KxlI0"
15
+ },
16
+ "internalVenue": "_ShRHGFliguTz51f7N_j69HDdgt-AI7U1LNG6c_uvwc",
17
+ "externalVenue": "DhI-4XuwkVxfbzp2tk7PgclUD7lVOJa-kNF2VVhLyDk",
18
+ "hunt": "kKpOdP--5hA3fy-i5q6-agoHFNKuYW4c_9Qg8NvAIew",
19
+ "huntWorkers": [
20
+ "kKpOdP--5hA3fy-i5q6-agoHFNKuYW4c_9Qg8NvAIew",
21
+ "NsIK9D0VScuNdf7-avycKxPSTEiHXPBlbFuCEv5ZEuw",
22
+ "JYtYuPA5weAbXykDnmDUhlU3LacxkY15qWppEDtTP4s",
23
+ "lJgsZ3GadofVafa8FI3C6fGTf6Aq0yfZFi44P6vRti0",
24
+ "GUMGAcSdCmKhb75M3NHUkLsdyoAHu7ixyuEldDsXA3c"
25
+ ],
26
+ "battleWorkers": [
27
+ "MgxNZ60Ejc01E5MeuZ1s_8PeLNhExjOz9wUGo3utE1w",
28
+ "Ww_GXme0Sn3-jIcji0dPxV0nXLYqBesChXqqXQTC0Hc",
29
+ "4YYgYXTzS_PjbQMthDwf93zik-kvoXYTzrNHA30ijAY",
30
+ "EWlpRzX-waNNKj0bvT9pzj_N4_bv2ndYPMOJaiVkWlw",
31
+ "v5_n4eOw24s1wGjI8-fjoGTfQHwjcO-tcHFxIiO5Qrk"
32
+ ]
33
+ },
34
+ "huntNode": "https://hyperbeam.tylerw.ai",
35
+ "marketNode": "https://hyperbeam.tylerw.ai",
36
+ "venueNode": "https://hyperbeam.tylerw.ai"
37
+ }
package/dist/http.mjs CHANGED
@@ -306,7 +306,7 @@ import { z as z9 } from "zod";
306
306
  import { z } from "zod";
307
307
 
308
308
  // src/update.mjs
309
- var VERSION = "0.3.2";
309
+ var VERSION = "0.4.2";
310
310
  var CHECK_MS = 6 * 36e5;
311
311
  var NOTE_MS = 36e5;
312
312
  var latest = null;
@@ -1042,16 +1042,30 @@ function registerExchange(server) {
1042
1042
  // src/bridge.mjs
1043
1043
  import crypto3 from "node:crypto";
1044
1044
  import { z as z4 } from "zod";
1045
- import { createPublicClient, createWalletClient, erc20Abi, http, parseAbi, formatEther } from "viem";
1045
+ import {
1046
+ createPublicClient,
1047
+ createWalletClient,
1048
+ encodeAbiParameters,
1049
+ erc20Abi,
1050
+ http,
1051
+ keccak256,
1052
+ parseAbi,
1053
+ formatEther,
1054
+ recoverMessageAddress,
1055
+ toBytes
1056
+ } from "viem";
1046
1057
  import { base } from "viem/chains";
1047
1058
  import {
1048
1059
  Connection,
1060
+ Ed25519Program,
1049
1061
  PublicKey,
1062
+ SYSVAR_INSTRUCTIONS_PUBKEY,
1050
1063
  SystemProgram,
1051
1064
  Transaction,
1052
1065
  TransactionInstruction,
1053
1066
  sendAndConfirmTransaction
1054
1067
  } from "@solana/web3.js";
1068
+ import { bcs } from "@mysten/sui/bcs";
1055
1069
  import { SuiGrpcClient } from "@mysten/sui/grpc";
1056
1070
  import { Transaction as SuiTransaction, coinWithBalance } from "@mysten/sui/transactions";
1057
1071
  var DAILY_FAUCET = 100000000n;
@@ -1096,6 +1110,30 @@ function baseChain(w) {
1096
1110
  const allowance = await pub.readContract({ address: token.address, abi: erc20Abi, functionName: "allowance", args: [me2, cfg.bridge] });
1097
1111
  if (allowance < amount) await write({ address: token.address, abi: erc20Abi, functionName: "approve", args: [cfg.bridge, amount] });
1098
1112
  return write({ address: cfg.bridge, abi: BRIDGE, functionName: "deposit", args: [token.address, amount, ao] });
1113
+ },
1114
+ /** Submit a signed withdrawal: the set's signatures, sorted by signer. */
1115
+ async claim(w2, set, namespace) {
1116
+ const RELEASE = parseAbi(["function release(uint64 withdrawalId, address token, address to, uint256 amount, bytes[] sigs)"]);
1117
+ const id = BigInt(namespace) << 32n | BigInt(w2.id);
1118
+ const inner = keccak256(encodeAbiParameters(
1119
+ [
1120
+ { type: "bytes32" },
1121
+ { type: "uint256" },
1122
+ { type: "address" },
1123
+ { type: "uint64" },
1124
+ { type: "address" },
1125
+ { type: "address" },
1126
+ { type: "uint256" }
1127
+ ],
1128
+ [keccak256(toBytes("PARALITH_RELEASE_V1")), BigInt(cfg.chainId), cfg.bridge, id, w2.token, w2.to, BigInt(w2.amount)]
1129
+ ));
1130
+ const allowed = new Set(set.map((m) => m.base.toLowerCase()));
1131
+ const rows = await Promise.all(set.filter((m) => w2.sigs?.[m.ao]).map(async (m) => ({
1132
+ sig: w2.sigs[m.ao],
1133
+ who: (await recoverMessageAddress({ message: { raw: inner }, signature: w2.sigs[m.ao] })).toLowerCase()
1134
+ })));
1135
+ const sigs = rows.filter((x) => allowed.has(x.who)).sort((a, b) => BigInt(a.who) < BigInt(b.who) ? -1 : 1).map((x) => x.sig);
1136
+ return write({ address: cfg.bridge, abi: RELEASE, functionName: "release", args: [id, w2.token, w2.to, BigInt(w2.amount), sigs] });
1099
1137
  }
1100
1138
  };
1101
1139
  }
@@ -1123,7 +1161,7 @@ function solanaChain(w) {
1123
1161
  len.writeUInt32LE(b.length);
1124
1162
  return Buffer.concat([len, b]);
1125
1163
  };
1126
- const send2 = (instruction) => sendAndConfirmTransaction(conn, new Transaction().add(instruction), [kp], { commitment: "confirmed" });
1164
+ const send2 = (...instructions) => sendAndConfirmTransaction(conn, new Transaction().add(...instructions), [kp], { commitment: "confirmed" });
1127
1165
  return {
1128
1166
  token,
1129
1167
  tokenTag: token.mint,
@@ -1168,7 +1206,55 @@ function solanaChain(w) {
1168
1206
  { pubkey: tokenProgram, isSigner: false, isWritable: false }
1169
1207
  ],
1170
1208
  data: Buffer.concat([ix("deposit"), u64(amount), str(ao)])
1171
- }))
1209
+ })),
1210
+ /** Submit a signed withdrawal: one Ed25519 precompile instruction holding the
1211
+ * members' signatures over the release message, then `release`. */
1212
+ claim(w2, set, namespace) {
1213
+ const id = BigInt(namespace) << 32n | BigInt(w2.id);
1214
+ const amount = BigInt(w2.amount);
1215
+ const to = new PublicKey(w2.to);
1216
+ const signed = set.filter((m) => w2.sigs?.[m.ao]);
1217
+ const msg = Buffer.concat([
1218
+ Buffer.from("PARALITH_RELEASE_V1"),
1219
+ program.toBuffer(),
1220
+ u64(id),
1221
+ mint.toBuffer(),
1222
+ to.toBuffer(),
1223
+ u64(amount)
1224
+ ]);
1225
+ const head = 2 + 14 * signed.length;
1226
+ const msgOff = head + 96 * signed.length;
1227
+ const data = Buffer.alloc(msgOff + msg.length);
1228
+ data[0] = signed.length;
1229
+ signed.forEach((m, i) => {
1230
+ const at = head + 96 * i;
1231
+ [at + 32, 65535, at, 65535, msgOff, msg.length, 65535].forEach((v, k) => data.writeUInt16LE(v, 2 + 14 * i + 2 * k));
1232
+ data.set(new PublicKey(m.solana).toBytes(), at);
1233
+ data.set(Buffer.from(w2.sigs[m.ao].replace(/^0x/, ""), "hex"), at + 32);
1234
+ });
1235
+ data.set(msg, msgOff);
1236
+ const toAta = PublicKey.findProgramAddressSync([to.toBuffer(), tokenProgram.toBuffer(), mint.toBuffer()], ATA_PROGRAM)[0];
1237
+ return send2(
1238
+ new TransactionInstruction({ programId: Ed25519Program.programId, keys: [], data }),
1239
+ new TransactionInstruction({
1240
+ programId: program,
1241
+ keys: [
1242
+ { pubkey: user, isSigner: true, isWritable: true },
1243
+ { pubkey: pda(Buffer.from("config")), isSigner: false, isWritable: false },
1244
+ { pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, isSigner: false, isWritable: false },
1245
+ { pubkey: pda(Buffer.from("receipt"), u64(id)), isSigner: false, isWritable: true },
1246
+ { pubkey: mint, isSigner: false, isWritable: false },
1247
+ { pubkey: pda(Buffer.from("vault"), mint.toBuffer()), isSigner: false, isWritable: true },
1248
+ { pubkey: to, isSigner: false, isWritable: false },
1249
+ { pubkey: toAta, isSigner: false, isWritable: true },
1250
+ { pubkey: tokenProgram, isSigner: false, isWritable: false },
1251
+ { pubkey: ATA_PROGRAM, isSigner: false, isWritable: false },
1252
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }
1253
+ ],
1254
+ data: Buffer.concat([ix("release"), u64(id), u64(amount)])
1255
+ })
1256
+ );
1257
+ }
1172
1258
  };
1173
1259
  }
1174
1260
  function suiChain(w) {
@@ -1216,6 +1302,25 @@ function suiChain(w) {
1216
1302
  arguments: [tx.object(cfg.bridge), coinWithBalance({ type: token.type, balance: amount }), tx.pure.string(ao)]
1217
1303
  });
1218
1304
  return exec(tx);
1305
+ },
1306
+ /** Submit a signed withdrawal: each signature with its signer's index in the set. */
1307
+ claim(w2, set, namespace) {
1308
+ const id = BigInt(namespace) << 32n | BigInt(w2.id);
1309
+ const signed = set.flatMap((m, at) => w2.sigs?.[m.ao] ? [{ at, sig: w2.sigs[m.ao] }] : []);
1310
+ const tx = new SuiTransaction();
1311
+ tx.moveCall({
1312
+ target: `${cfg.package}::para_bridge::release`,
1313
+ typeArguments: [w2.token],
1314
+ arguments: [
1315
+ tx.object(cfg.bridge),
1316
+ tx.pure.u64(id),
1317
+ tx.pure.u64(BigInt(w2.amount)),
1318
+ tx.pure.address(w2.to),
1319
+ tx.pure(bcs.vector(bcs.u64()).serialize(signed.map((x) => BigInt(x.at)))),
1320
+ tx.pure(bcs.vector(bcs.vector(bcs.u8())).serialize(signed.map((x) => Array.from(Buffer.from(x.sig.replace(/^0x/, ""), "hex")))))
1321
+ ]
1322
+ });
1323
+ return exec(tx);
1219
1324
  }
1220
1325
  };
1221
1326
  }
@@ -1241,7 +1346,7 @@ function registerBridge(server) {
1241
1346
  tool(
1242
1347
  server,
1243
1348
  "bridge_info",
1244
- "PUSD bridge configuration and ledger: fees (bps, in/out), supply, reserves per chain and token, relayer, and each chain's contract addresses. Free.",
1349
+ "PUSD bridge configuration and ledger: fees (bps, in/out), supply, reserves per chain and token, the attestor set, and each chain's contract addresses. Free.",
1245
1350
  {},
1246
1351
  async () => ({
1247
1352
  pusd: PUSD,
@@ -1290,17 +1395,12 @@ function registerBridge(server) {
1290
1395
  },
1291
1396
  WRITE
1292
1397
  );
1293
- const RELAYER_WATCH = process.env.PARALITH_RELAYER_WATCH || "https://bridge.paralithmarkets.com/relay/watch";
1294
- const watchDeposit = (chain, tx) => fetch(RELAYER_WATCH, {
1295
- method: "POST",
1296
- headers: { "content-type": "application/json" },
1297
- body: JSON.stringify({ chain, tx }),
1298
- signal: AbortSignal.timeout(3e4)
1299
- }).then((r) => r.ok, () => false);
1398
+ const nudgeNodes = () => Promise.all((chains.nodes ?? []).map((n) => fetch(`${n.url}/~bridge@1.0/nudge`, { signal: AbortSignal.timeout(1e4) }).catch(() => {
1399
+ })));
1300
1400
  tool(
1301
1401
  server,
1302
1402
  "bridge_deposit",
1303
- "Bridge TEST-USDC from a chain into PUSD on your AO address: approve if needed, lock in the bridge contract, then wait until the relayer credits PUSD (inbound fee applies; see bridge_info). Needs native gas.",
1403
+ "Bridge TEST-USDC from a chain into PUSD on your AO address: approve if needed, lock in the bridge contract, then wait until k attestor nodes report it and PUSD credits you (inbound fee applies; see bridge_info). Needs native gas.",
1304
1404
  { chain: CHAIN, amount: z4.string(), wait: z4.boolean().optional().describe("Default true.") },
1305
1405
  async ({ chain, amount, wait = true }) => {
1306
1406
  const { wallets } = await runtime();
@@ -1308,8 +1408,8 @@ function registerBridge(server) {
1308
1408
  const atoms = parseAmount(amount);
1309
1409
  const before = await pusdBalance(wallets.ao.address);
1310
1410
  const tx = await c.deposit(atoms, wallets.ao.address);
1311
- await watchDeposit(chain, tx);
1312
- if (!wait) return { chain, tx, note: "Locked on the source chain. PUSD arrives when the relayer attests; check bridge_balances." };
1411
+ await nudgeNodes();
1412
+ if (!wait) return { chain, tx, note: "Locked on the source chain. PUSD arrives when k nodes attest; check bridge_balances." };
1313
1413
  const after = await until("PUSD credit", () => pusdBalance(wallets.ao.address), (b) => b > before, 9e5);
1314
1414
  return { chain, tx, credited: formatAtoms(after - before), pusd: formatAtoms(after) };
1315
1415
  },
@@ -1318,11 +1418,12 @@ function registerBridge(server) {
1318
1418
  tool(
1319
1419
  server,
1320
1420
  "bridge_redeem",
1321
- "Redeem PUSD to TEST-USDC on a chain (outbound fee, see bridge_info; the destination reserve must cover it). Sends to your own address on that chain unless recipient is given; waits for the release unless wait is false.",
1421
+ "Redeem PUSD to TEST-USDC on a chain (outbound fee, see bridge_info; the destination reserve must cover it). Sends to your own address on that chain unless recipient is given. Unless wait is false, waits for k node signatures and submits the release from your wallet on that chain (needs its native gas).",
1322
1422
  { chain: CHAIN, amount: z4.string(), recipient: z4.string().optional(), wait: z4.boolean().optional() },
1323
1423
  async ({ chain, amount, recipient, wait = true }) => {
1324
1424
  const { api } = await runtime();
1325
1425
  const c = await chainFor(chain);
1426
+ await nudgeNodes();
1326
1427
  const to = recipient ?? c.recipient;
1327
1428
  const own = to === c.recipient;
1328
1429
  const before = own ? await c.balance() : null;
@@ -1336,13 +1437,21 @@ function registerBridge(server) {
1336
1437
  if (!reply || reply.error) throw new Error(`Bridge-Redeem refused: ${reply?.error ?? "no reply"}`);
1337
1438
  const out = { chain, withdrawalId: reply.WithdrawalId, net: formatAtoms(reply.Quantity ?? 0), fee: formatAtoms(reply.Fee ?? 0), recipient: to };
1338
1439
  if (!wait) return out;
1339
- await until(
1340
- "release",
1440
+ const info = await readKey(PUSD, "bridgeinfo");
1441
+ const set = info?.attestors ?? [];
1442
+ const k = Math.floor((3 * set.length + 4) / 5);
1443
+ const signed = (w2) => set.filter((m) => w2?.sigs?.[m.ao]).length;
1444
+ const list = await until(
1445
+ `${k} node signatures`,
1341
1446
  () => readKey(PUSD, "withdrawals"),
1342
- (list) => (list ?? []).some((w) => w.id === String(reply.WithdrawalId) && w.status === "released"),
1447
+ (l) => signed((l ?? []).find((w2) => w2.id === String(reply.WithdrawalId))) >= k,
1343
1448
  9e5
1344
1449
  );
1345
- return { ...out, status: "released", ...own ? { received: formatAtoms(await c.balance() - before) } : {} };
1450
+ const w = list.find((x) => x.id === String(reply.WithdrawalId));
1451
+ const tx = await c.claim(w, set, Number(info.namespace));
1452
+ if (!own) return { ...out, status: "released", tx };
1453
+ const after = await until("release", () => c.balance(), (b) => b >= before + BigInt(w.amount), 3e5);
1454
+ return { ...out, status: "released", tx, received: formatAtoms(after - before) };
1346
1455
  },
1347
1456
  WRITE
1348
1457
  );
package/dist/server.mjs CHANGED
@@ -529,7 +529,7 @@ var VERSION, CHECK_MS, NOTE_MS, latest, checkedAt, notedAt, newer;
529
529
  var init_update = __esm({
530
530
  "src/update.mjs"() {
531
531
  "use strict";
532
- VERSION = "0.3.2";
532
+ VERSION = "0.4.2";
533
533
  CHECK_MS = 6 * 36e5;
534
534
  NOTE_MS = 36e5;
535
535
  latest = null;
@@ -1284,16 +1284,30 @@ var init_exchange = __esm({
1284
1284
  // src/bridge.mjs
1285
1285
  import crypto3 from "node:crypto";
1286
1286
  import { z as z4 } from "zod";
1287
- import { createPublicClient, createWalletClient, erc20Abi, http, parseAbi, formatEther } from "viem";
1287
+ import {
1288
+ createPublicClient,
1289
+ createWalletClient,
1290
+ encodeAbiParameters,
1291
+ erc20Abi,
1292
+ http,
1293
+ keccak256,
1294
+ parseAbi,
1295
+ formatEther,
1296
+ recoverMessageAddress,
1297
+ toBytes
1298
+ } from "viem";
1288
1299
  import { base } from "viem/chains";
1289
1300
  import {
1290
1301
  Connection,
1302
+ Ed25519Program,
1291
1303
  PublicKey,
1304
+ SYSVAR_INSTRUCTIONS_PUBKEY,
1292
1305
  SystemProgram,
1293
1306
  Transaction,
1294
1307
  TransactionInstruction,
1295
1308
  sendAndConfirmTransaction
1296
1309
  } from "@solana/web3.js";
1310
+ import { bcs } from "@mysten/sui/bcs";
1297
1311
  import { SuiGrpcClient } from "@mysten/sui/grpc";
1298
1312
  import { Transaction as SuiTransaction, coinWithBalance } from "@mysten/sui/transactions";
1299
1313
  function baseChain(w) {
@@ -1333,6 +1347,30 @@ function baseChain(w) {
1333
1347
  const allowance = await pub.readContract({ address: token.address, abi: erc20Abi, functionName: "allowance", args: [me2, cfg.bridge] });
1334
1348
  if (allowance < amount) await write({ address: token.address, abi: erc20Abi, functionName: "approve", args: [cfg.bridge, amount] });
1335
1349
  return write({ address: cfg.bridge, abi: BRIDGE, functionName: "deposit", args: [token.address, amount, ao] });
1350
+ },
1351
+ /** Submit a signed withdrawal: the set's signatures, sorted by signer. */
1352
+ async claim(w2, set, namespace) {
1353
+ const RELEASE = parseAbi(["function release(uint64 withdrawalId, address token, address to, uint256 amount, bytes[] sigs)"]);
1354
+ const id = BigInt(namespace) << 32n | BigInt(w2.id);
1355
+ const inner = keccak256(encodeAbiParameters(
1356
+ [
1357
+ { type: "bytes32" },
1358
+ { type: "uint256" },
1359
+ { type: "address" },
1360
+ { type: "uint64" },
1361
+ { type: "address" },
1362
+ { type: "address" },
1363
+ { type: "uint256" }
1364
+ ],
1365
+ [keccak256(toBytes("PARALITH_RELEASE_V1")), BigInt(cfg.chainId), cfg.bridge, id, w2.token, w2.to, BigInt(w2.amount)]
1366
+ ));
1367
+ const allowed = new Set(set.map((m) => m.base.toLowerCase()));
1368
+ const rows = await Promise.all(set.filter((m) => w2.sigs?.[m.ao]).map(async (m) => ({
1369
+ sig: w2.sigs[m.ao],
1370
+ who: (await recoverMessageAddress({ message: { raw: inner }, signature: w2.sigs[m.ao] })).toLowerCase()
1371
+ })));
1372
+ const sigs = rows.filter((x) => allowed.has(x.who)).sort((a, b) => BigInt(a.who) < BigInt(b.who) ? -1 : 1).map((x) => x.sig);
1373
+ return write({ address: cfg.bridge, abi: RELEASE, functionName: "release", args: [id, w2.token, w2.to, BigInt(w2.amount), sigs] });
1336
1374
  }
1337
1375
  };
1338
1376
  }
@@ -1360,7 +1398,7 @@ function solanaChain(w) {
1360
1398
  len.writeUInt32LE(b.length);
1361
1399
  return Buffer.concat([len, b]);
1362
1400
  };
1363
- const send2 = (instruction) => sendAndConfirmTransaction(conn, new Transaction().add(instruction), [kp], { commitment: "confirmed" });
1401
+ const send2 = (...instructions) => sendAndConfirmTransaction(conn, new Transaction().add(...instructions), [kp], { commitment: "confirmed" });
1364
1402
  return {
1365
1403
  token,
1366
1404
  tokenTag: token.mint,
@@ -1405,7 +1443,55 @@ function solanaChain(w) {
1405
1443
  { pubkey: tokenProgram, isSigner: false, isWritable: false }
1406
1444
  ],
1407
1445
  data: Buffer.concat([ix("deposit"), u64(amount), str(ao)])
1408
- }))
1446
+ })),
1447
+ /** Submit a signed withdrawal: one Ed25519 precompile instruction holding the
1448
+ * members' signatures over the release message, then `release`. */
1449
+ claim(w2, set, namespace) {
1450
+ const id = BigInt(namespace) << 32n | BigInt(w2.id);
1451
+ const amount = BigInt(w2.amount);
1452
+ const to = new PublicKey(w2.to);
1453
+ const signed = set.filter((m) => w2.sigs?.[m.ao]);
1454
+ const msg = Buffer.concat([
1455
+ Buffer.from("PARALITH_RELEASE_V1"),
1456
+ program.toBuffer(),
1457
+ u64(id),
1458
+ mint.toBuffer(),
1459
+ to.toBuffer(),
1460
+ u64(amount)
1461
+ ]);
1462
+ const head = 2 + 14 * signed.length;
1463
+ const msgOff = head + 96 * signed.length;
1464
+ const data = Buffer.alloc(msgOff + msg.length);
1465
+ data[0] = signed.length;
1466
+ signed.forEach((m, i) => {
1467
+ const at = head + 96 * i;
1468
+ [at + 32, 65535, at, 65535, msgOff, msg.length, 65535].forEach((v, k) => data.writeUInt16LE(v, 2 + 14 * i + 2 * k));
1469
+ data.set(new PublicKey(m.solana).toBytes(), at);
1470
+ data.set(Buffer.from(w2.sigs[m.ao].replace(/^0x/, ""), "hex"), at + 32);
1471
+ });
1472
+ data.set(msg, msgOff);
1473
+ const toAta = PublicKey.findProgramAddressSync([to.toBuffer(), tokenProgram.toBuffer(), mint.toBuffer()], ATA_PROGRAM)[0];
1474
+ return send2(
1475
+ new TransactionInstruction({ programId: Ed25519Program.programId, keys: [], data }),
1476
+ new TransactionInstruction({
1477
+ programId: program,
1478
+ keys: [
1479
+ { pubkey: user, isSigner: true, isWritable: true },
1480
+ { pubkey: pda(Buffer.from("config")), isSigner: false, isWritable: false },
1481
+ { pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, isSigner: false, isWritable: false },
1482
+ { pubkey: pda(Buffer.from("receipt"), u64(id)), isSigner: false, isWritable: true },
1483
+ { pubkey: mint, isSigner: false, isWritable: false },
1484
+ { pubkey: pda(Buffer.from("vault"), mint.toBuffer()), isSigner: false, isWritable: true },
1485
+ { pubkey: to, isSigner: false, isWritable: false },
1486
+ { pubkey: toAta, isSigner: false, isWritable: true },
1487
+ { pubkey: tokenProgram, isSigner: false, isWritable: false },
1488
+ { pubkey: ATA_PROGRAM, isSigner: false, isWritable: false },
1489
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }
1490
+ ],
1491
+ data: Buffer.concat([ix("release"), u64(id), u64(amount)])
1492
+ })
1493
+ );
1494
+ }
1409
1495
  };
1410
1496
  }
1411
1497
  function suiChain(w) {
@@ -1453,6 +1539,25 @@ function suiChain(w) {
1453
1539
  arguments: [tx.object(cfg.bridge), coinWithBalance({ type: token.type, balance: amount }), tx.pure.string(ao)]
1454
1540
  });
1455
1541
  return exec(tx);
1542
+ },
1543
+ /** Submit a signed withdrawal: each signature with its signer's index in the set. */
1544
+ claim(w2, set, namespace) {
1545
+ const id = BigInt(namespace) << 32n | BigInt(w2.id);
1546
+ const signed = set.flatMap((m, at) => w2.sigs?.[m.ao] ? [{ at, sig: w2.sigs[m.ao] }] : []);
1547
+ const tx = new SuiTransaction();
1548
+ tx.moveCall({
1549
+ target: `${cfg.package}::para_bridge::release`,
1550
+ typeArguments: [w2.token],
1551
+ arguments: [
1552
+ tx.object(cfg.bridge),
1553
+ tx.pure.u64(id),
1554
+ tx.pure.u64(BigInt(w2.amount)),
1555
+ tx.pure.address(w2.to),
1556
+ tx.pure(bcs.vector(bcs.u64()).serialize(signed.map((x) => BigInt(x.at)))),
1557
+ tx.pure(bcs.vector(bcs.vector(bcs.u8())).serialize(signed.map((x) => Array.from(Buffer.from(x.sig.replace(/^0x/, ""), "hex")))))
1558
+ ]
1559
+ });
1560
+ return exec(tx);
1456
1561
  }
1457
1562
  };
1458
1563
  }
@@ -1477,7 +1582,7 @@ function registerBridge(server) {
1477
1582
  tool(
1478
1583
  server,
1479
1584
  "bridge_info",
1480
- "PUSD bridge configuration and ledger: fees (bps, in/out), supply, reserves per chain and token, relayer, and each chain's contract addresses. Free.",
1585
+ "PUSD bridge configuration and ledger: fees (bps, in/out), supply, reserves per chain and token, the attestor set, and each chain's contract addresses. Free.",
1481
1586
  {},
1482
1587
  async () => ({
1483
1588
  pusd: PUSD,
@@ -1526,17 +1631,12 @@ function registerBridge(server) {
1526
1631
  },
1527
1632
  WRITE
1528
1633
  );
1529
- const RELAYER_WATCH = process.env.PARALITH_RELAYER_WATCH || "https://bridge.paralithmarkets.com/relay/watch";
1530
- const watchDeposit = (chain, tx) => fetch(RELAYER_WATCH, {
1531
- method: "POST",
1532
- headers: { "content-type": "application/json" },
1533
- body: JSON.stringify({ chain, tx }),
1534
- signal: AbortSignal.timeout(3e4)
1535
- }).then((r) => r.ok, () => false);
1634
+ const nudgeNodes = () => Promise.all((chains.nodes ?? []).map((n) => fetch(`${n.url}/~bridge@1.0/nudge`, { signal: AbortSignal.timeout(1e4) }).catch(() => {
1635
+ })));
1536
1636
  tool(
1537
1637
  server,
1538
1638
  "bridge_deposit",
1539
- "Bridge TEST-USDC from a chain into PUSD on your AO address: approve if needed, lock in the bridge contract, then wait until the relayer credits PUSD (inbound fee applies; see bridge_info). Needs native gas.",
1639
+ "Bridge TEST-USDC from a chain into PUSD on your AO address: approve if needed, lock in the bridge contract, then wait until k attestor nodes report it and PUSD credits you (inbound fee applies; see bridge_info). Needs native gas.",
1540
1640
  { chain: CHAIN, amount: z4.string(), wait: z4.boolean().optional().describe("Default true.") },
1541
1641
  async ({ chain, amount, wait = true }) => {
1542
1642
  const { wallets } = await runtime();
@@ -1544,8 +1644,8 @@ function registerBridge(server) {
1544
1644
  const atoms = parseAmount(amount);
1545
1645
  const before = await pusdBalance(wallets.ao.address);
1546
1646
  const tx = await c.deposit(atoms, wallets.ao.address);
1547
- await watchDeposit(chain, tx);
1548
- if (!wait) return { chain, tx, note: "Locked on the source chain. PUSD arrives when the relayer attests; check bridge_balances." };
1647
+ await nudgeNodes();
1648
+ if (!wait) return { chain, tx, note: "Locked on the source chain. PUSD arrives when k nodes attest; check bridge_balances." };
1549
1649
  const after = await until("PUSD credit", () => pusdBalance(wallets.ao.address), (b) => b > before, 9e5);
1550
1650
  return { chain, tx, credited: formatAtoms(after - before), pusd: formatAtoms(after) };
1551
1651
  },
@@ -1554,11 +1654,12 @@ function registerBridge(server) {
1554
1654
  tool(
1555
1655
  server,
1556
1656
  "bridge_redeem",
1557
- "Redeem PUSD to TEST-USDC on a chain (outbound fee, see bridge_info; the destination reserve must cover it). Sends to your own address on that chain unless recipient is given; waits for the release unless wait is false.",
1657
+ "Redeem PUSD to TEST-USDC on a chain (outbound fee, see bridge_info; the destination reserve must cover it). Sends to your own address on that chain unless recipient is given. Unless wait is false, waits for k node signatures and submits the release from your wallet on that chain (needs its native gas).",
1558
1658
  { chain: CHAIN, amount: z4.string(), recipient: z4.string().optional(), wait: z4.boolean().optional() },
1559
1659
  async ({ chain, amount, recipient, wait = true }) => {
1560
1660
  const { api } = await runtime();
1561
1661
  const c = await chainFor(chain);
1662
+ await nudgeNodes();
1562
1663
  const to = recipient ?? c.recipient;
1563
1664
  const own = to === c.recipient;
1564
1665
  const before = own ? await c.balance() : null;
@@ -1572,13 +1673,21 @@ function registerBridge(server) {
1572
1673
  if (!reply || reply.error) throw new Error(`Bridge-Redeem refused: ${reply?.error ?? "no reply"}`);
1573
1674
  const out = { chain, withdrawalId: reply.WithdrawalId, net: formatAtoms(reply.Quantity ?? 0), fee: formatAtoms(reply.Fee ?? 0), recipient: to };
1574
1675
  if (!wait) return out;
1575
- await until(
1576
- "release",
1676
+ const info = await readKey(PUSD, "bridgeinfo");
1677
+ const set = info?.attestors ?? [];
1678
+ const k = Math.floor((3 * set.length + 4) / 5);
1679
+ const signed = (w2) => set.filter((m) => w2?.sigs?.[m.ao]).length;
1680
+ const list = await until(
1681
+ `${k} node signatures`,
1577
1682
  () => readKey(PUSD, "withdrawals"),
1578
- (list) => (list ?? []).some((w) => w.id === String(reply.WithdrawalId) && w.status === "released"),
1683
+ (l) => signed((l ?? []).find((w2) => w2.id === String(reply.WithdrawalId))) >= k,
1579
1684
  9e5
1580
1685
  );
1581
- return { ...out, status: "released", ...own ? { received: formatAtoms(await c.balance() - before) } : {} };
1686
+ const w = list.find((x) => x.id === String(reply.WithdrawalId));
1687
+ const tx = await c.claim(w, set, Number(info.namespace));
1688
+ if (!own) return { ...out, status: "released", tx };
1689
+ const after = await until("release", () => c.balance(), (b) => b >= before + BigInt(w.amount), 3e5);
1690
+ return { ...out, status: "released", tx, received: formatAtoms(after - before) };
1582
1691
  },
1583
1692
  WRITE
1584
1693
  );